A GALSync powershell script

Here is a script I wrote to do a simple GAL synchronization between two Exchange organizations. The script finds the mail-enabled  users in one domain, and creates contacts for them in the other domain. Existing contacts will also be updated and deleted as needed.


There was a bug in the original script where I had forgotten to populate mailNickname. I have now done so, adding a “c-” to the front of it as a completely optional convention to avoid conflicts.

Update 2

Several people have commented below about needing to enable the contacts in Exchange after creation. I have used the modifications posted by Mark in the comments to make a new version that I hope will work better with 2007 and 2010, though I have only tested it with 2010. Both versions are linked below, and please keep adding your comments and modifications.

The Script

Now I have two versions the scripts have been moved off to separate pages. Follow the links below.

Version 2 Added the Update-Recipient command for Exchange 2007 (through local PowerShell) and Exchange 2010 (through remote PowerShell).
Version 1 Original version that uses the ActiveDirectory PowerShell module to create the contact objects.

Other people’s versions

Modified for Distribution Lists: https://www.wapshere.com/missmiis/galsync-v2/galsync-ps1-for-distribution-lists

78 Replies to “A GALSync powershell script”

  1. Very nice! How would I modify this script for a one-way sync? DOMAIN2 Users –> DOMAIN1 Contacts only?


  2. Hi Eric. That is simple enough – you see at the bottom of the script I have called the SyncContacts function twice – once for each domain? Just call it once.

  3. Tried running this – nogo on Exchange server – nogo on DC – currently loading Exchange tools to DC and will try again. RSAT tools loaded in all cases … Any thoughts in the mean time much appreciated 😀


  4. I am not running this on an Exchange server or on a DC – just on a regular member server with the AD powershell plugin available.

    Any errors?

  5. Sorry 🙁 any clues? E2k7SP2 on Win2k8StdSp2 returns:

    PS C:\> .\GALScript.ps1
    The term ‘get-module’ is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again.
    At C:\GALScript.ps1:179 char:16
    + if(@(get-module <<<< | where-object {$_.Name -eq "ActiveDirectory"} ).count -eq 0) {import-module ActiveDirectory}
    The term 'import-module' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again.
    At C:\GALScript.ps1:179 char:93
    + if(@(get-module | where-object {$_.Name -eq "ActiveDirectory"} ).count -eq 0) {import-module <<< DOMAIN2 Contacts

    DOMAIN2 Users –> DOMAIN1 Contacts
    The term ‘Get-ADObject’ is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again.
    At C:\GALScript.ps1:64 char:26
    + $colUsers = Get-ADObject <<< stop-transcript
    Transcription has not been started. Use the start-transcript command to start transcription.
    Stop-Transcript : An error occurred stopping transcription: The console host is not currently transcribing.
    At line:1 char:15
    + stop-transcript <<<

  6. It now thinks source user password is Null and finds no users in the source domain – this from a Windows7x64 pc on domain using either domain access account set up when I get a 2-box challenge come up when I run it. I’m trying to find a Win2k8 server with ADWeb services running on it …


    PS D:\scripts> .\GALScript.ps1
    DOMAIN1 Users –> DOMAIN2 Contacts

    DOMAIN2 Users –> DOMAIN1 Contacts
    ConvertTo-SecureString : Key not valid for use in specified state.
    At D:\scripts\GALScript.ps1:61 char:64
    + $password = get-content $sourcePWFile | convertto-securestring <<<<
    + CategoryInfo : InvalidArgument: (:) [ConvertTo-SecureString], CryptographicException
    + FullyQualifiedErrorId : ImportSecureString_InvalidArgument_CryptographicError,Microsoft.PowerShell.Commands.Conv

    New-Object : Exception calling ".ctor" with "2" argument(s): "Cannot process argument because the value of argument "pa
    ssword" is null. Change the value of argument "password" to a non-null value."
    At D:\scripts\GALScript.ps1:62 char:27
    + $sourceCred = New-Object <<<< -Typename System.Management.Automation.PSCredential -Argumentlist $sourceUser,$pa
    + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

    Get-ADObject : Unable to contact the server. This may be because this server does not exist, it is currently down, or i
    t does not have the Active Directory Web Services running.
    At D:\scripts\GALScript.ps1:64 char:26
    + $colUsers = Get-ADObject <<<

  7. You need to create the secure files as descibed in the comments at the top of the script, and also they must be secured with the account that you then run the script with. The idea is that you can store the password in a secure file rather than hardcoding it in the script.

    I suggest you create your cred file and then try and run some of the cmdlets manually using it – such as get-adobject. Once you can do that successfully with the cred file and with the account you mean to run the script with, then the script should work.

  8. You also need AD Web Services on a Win2k8R2 DC to target the ActiveDirectory module so this script only seems to work for two Windows 2008 R2 controlled domains.

    Any thoughts/comments?

  9. PS Storming work here by the way! Despite the fact I can’t get it to run, it’s the first time I’ve found anyone take on anything like this!! It would seem that despite Microsft lauding the programmability of AD/Exchange using Powershell that they don’t want us to use it for anything worthwhile. It remains the realm of die-hards and technical staff from other areas to even try and come up with a solution like this.

    Now to see if I can get to something similar in QAD Powershell for pre-2k8R2 bodies …

  10. It’s possible. The two domains I’m running it against have Windows 2008r2 DCs in them. It doesn’t mention such a requirement in that Scripting Guys page I linked to, but of course that doesn’t mean it’s not there.

    You could probably re-write the script using the Quest AD cmdlets. Alternatively it is possible to create AD objects with powershell using ADSI – just not as neat as with this plugin.

    And thanks! 🙂

  11. Getting there slowly … QAD command set is a bit twisty and I had to filter using LDAP rather than OLAP so someone will have an opportunity to clean some things up.

    Most of the commands are transferring ok but I’m restricted to doing it in my lunchtime hance the phenominal speed (ha…)

    PS why use target address instead of mail when working with Contacts?

  12. Hello Guys,

    I have a situation where Forest 1: Windows 2008 SP2 w/Exchange 2010 and Forest 2:Windows 2003 R2 w/Exchange 2003. Cross forest migration. Ran into a problem with native tool support on GAL Sync and F/B. IIFP does not work and same goes to InterOrg. Decided to try this script. But unfortunately, it requires Windows 2008 R2 DC. With above problem, is there a chance that 3rd forest promoted and link with transitive forest trust and this script is executable?

  13. If you can’t get a 2008r2 DC into the 2003 forest (doesn’t require a functional level upgrade or anything) then the only thing I can suggest is re-writing the script so it uses the powershell v1 ADSI methods instead. They’re a bit more long winded but the result should be the same.

  14. Hi Tom/Carol … This shouldn’t need a 3rd forest as long as the (unfortunately re-written in Powershell) script can hit both domains using LDAP from the Windows 2008 SP2 end. MIISFP doesn’t work against W2k8 and FIM GalSynch process is chewy. Good luck.

  15. Hi Carol. Awesome script! Thanks for sharing it!

    You don’t need Server 2008 R2 in both forests. You can download Active Directory Management Gateway Service for Server 2003 and 2008 from Microsoft (http://www.microsoft.com/downloads/en/details.aspx?FamilyID=008940c6-0296-4597-be3e-1d24c1cf0dda&displaylang=en). Read the entire article because there are prerequisites and it may require a reboot.

    The script creates Active Directory contacts. If you want them to show up in the Global Address List and Offline Address Book, you need to mail enable the contacts. To do that I added this line after the New-ADObject command in the ADDS loop.

    Get-Contact $user.displayName | foreach {Enable-MailContact $_ -externalEmailAddress $_.windowsEmailAddress.toString()}

    Then I ran the script from the Exchange Management Shell. It worked perfectly between a Server 2003 forest and a Server 2008 R2 forest. The only thing I am not sure about yet is the updating part.

  16. Has anyone tried this for use with Live@edu or outlook online?

    I am looking for an alternative to ILM 2007 or FIM for syncing on site AD with outlook online. I want it to create mailboxes and sync passwords.

  17. We have two forest that has an two way trust. In both forest the domain controllers are allwindows 2008 R2 servers. The syncs users -> contacts from domain1 to domain2 works. But when users want to send an email to the contact that they find in the GAL they get an error. Unable to relay. When i look into the contact details even with adsi edit everthing looks ok. Have this in two place’s a freshly setup test enviroment and our live “production” enviroment. Are there other people who has the same issue and what can i do against it. I want to make the contacts usable in outlook and not only viewable in “active directory users and computers”.

  18. To start with make sure the contacts appear in the Exchange management console. If they do then the contacts are created correcty and you need to look to your Exchange settings.

  19. I get this failure message back when i send a test mail to an imported contact.

    Diagnostische gegevens voor beheerders:

    Bronserver: MAIL01.thereferencegroup.be

    #550 5.1.1 RESOLVER.ADR.ExRecipNotFound; not found ##

    Oorspronkelijke berichtkoppen:

    Received: from MAIL01.thereferencegroup.be ([]) by
    MAIL01.thereferencegroup.be ([]) with mapi; Fri, 4 Mar 2011
    08:32:05 +0100
    Content-Type: application/ms-tnef; name=”winmail.dat”
    Content-Transfer-Encoding: binary
    From: Arne Verstraete
    To: ipmonitor
    Subject: test
    Thread-Topic: test
    Thread-Index: AcvaPj5da7PNA7qdTj2Gx7Q9hT/n8A==
    Date: Fri, 4 Mar 2011 08:32:03 +0100
    Accept-Language: nl-BE, en-US
    Content-Language: en-US
    MIME-Version: 1.0

  20. I suggest you create a contact manually through Exchange and see if you get the same problem. If it works then inspect both contacts (the one created manually and the one created with this script) using ADSIEdit to see what the differences are. It’s possible you may need to modify the script to either add some extra attributes, or to run an update-recipient after the contact creation. I haven’t found that necessary but I’ve only run this in one environment. The script is posted here as an example only and you need to adjust it for your own environment.

  21. When i create a contact through Exchange I do not have the same problem. But i will test with the update-recipient cmdlet. I keep you informed. Thanks for the help in advance

  22. awesome script! Any reason why I couldn’t lose the credentials portion of this script so that they are not stored in a text file, but stored with the task service? I know there are a few things *cough* that need to be done to enable PS1 files to be ran from EMS as a scheduled task, but I am pretty sure that would work, no?

  23. I’m sure you could. The script only uses AD methods deliberately to try and simplify the permission side of things, though a couple of people have commented that they needed to run enable-mailcontact afterwards, which would need the exchange plugins as well as exchange recipient admin, so I guess it depends on the environment. All you can do is try!

  24. The script as written requires no exchange admin permission because it is only creating AD objects. However some people have mentioned needing to run enable-mailcontact after the contact has been created. If you find you need to do this then you will also need to load the Exchange powershell snapin and the account will need Recipient Admin permissions.

  25. I solved my problem just added the two lines below in the ADDS loop just after New-ADObject

    Get-MailContact $User.displayname | foreach {Disable-Mailcontact $_ -Confirm:$false}
    Get-Contact $user.displayName | foreach {Enable-MailContact $_ -externalEmailAddress $_.windowsEmailAddress.toString()}

  26. Thank you Carol for clarification.

    I am trying to create contacts in my domain (target) of users who are residing from one particular OU only in the remote domain (source) How I can achieve this? Currently the script is creating contacts of all users from the source domain.

    Thanking you,

  27. Thanks Carol, but when i added -SearchBase as below
    $colUsers = Get-ADObject -Filter $strSelectUsers -SearchBase ‘OU=Test,DC=xyz,DC=com’ -Properties * -Server $sourceDC -Credential $sourceCred

    I am getting error as it is only works for xyz.com and not abcd.com

    please clarify.

    thanking you, guruprasad.

  28. Then you need to modify the SyncContacts function so you can pass the searchbase as a parameter.

  29. Finally Carol I am running individual scripts respective to each domain.

    Thank you for all the replies and the great work.

    Allow me to get back to you in case if I require your help.

    Thanking you, Guru.

  30. Hi Carol,

    As Guruprasad said the script will find the users in Domain A and create the contacts for the users in Domain B. We can modify the script and allow to find users in only one OU and create the contacts for users only in the OU. My question is I have a dynamic distribution list which contain some users as member. The script need to find all the dynamic distribution list members and create contacts only for those users. How to achieve this. If I directly went to EMC I am able to get all members using the below command.

    $group = Get-DynamicDistributionGroup –identity “AllStaff”

    Get-Recipient –RecipientPreviewFilter $group.RecipientFilter

    How to insert the above commands in script?


  31. Great Script Carol!

    I’m working in test environment to find the best solution for our company merger and came accross your script.

    I have an Exchange 2010 server in each forest.

    FIM2010 had been attempted, but I didn’t get far with it before I found your script and decided to try it out.

    When I ran the GALSync script the contacts were created, but not mail enabled (As Ron stated.. Unfortunately I didn’t see his comments until today)

    I was able to substitue remote Exchange 2010 powershell commands to create mail enabled contacts and wanted to share my tweaks in case anyone was interested. These tweaks only modified the ADD functionality, leaving the DELETE and UPDATE sections alone.

    Added the following two variables to the Global Definitions section:

    I modified the PARAM line of the SyncContacts Function to add $targetURI:
    PARAM($sourceDC, $sourceUser, $sourcePWFile, $targetDC, $targetUser, $targetPWFile, $targetOU, $targetURI)

    The New-ADObject line was commented out in the ADDS line and the following thre lines were added:
    $SO=New-PSSessionOption -SkipCACheck -SkipCNCheck –SkipRevocationCheck –ProxyAccessType None

    $PSSession1=New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $targetURI -Credential $targetCred -SessionOption $SO

    invoke-Command -Session $PSSession1 -ScriptBlock{param ($User,$targetOU,$targetaddress) New-MailContact -Name $User.displayname -OrganizationalUnit $targetOU -ExternalEmailAddress $targetAddress} -ArgumentList $User,$targetOU,$targetaddress

    In the MAIN section the SyncContacts lines were modified to add the $targetURI and $sourceURI parameter
    SyncContacts -sourceDC $DOMAIN_1 -sourceUser $USER_1 -sourcePWFile $PWFILE_1 -targetDC $DOMAIN_2 -targetUser $USER_2 -targetPWFile $PWFILE_2 -targetOU $OU_CONTACTS_2 $targetURI

    SyncContacts -sourceDC $DOMAIN_2 -sourceUser $USER_2 -sourcePWFile $PWFILE_2 -targetDC $DOMAIN_1 -targetUser $USER_1 -targetPWFile $PWFILE_1 -targetOU $OU_CONTACTS_1 $sourceURI

    I was also able to get Free/Busy shared between forests using the Microsoft “Configure the Availability Service for Cross-Forest Topologies” technote.

    Who Needs FIM 2010 🙂

    Thanks Again Carol

  32. Thanks very much for this Mark. I have merged the two comments you made so that the line you mentioned is correct in your orignial post. I will update the script in the post as soon as I get a chance to test your modifications. I was hoping to get away with not having to load the Exchange plugin and give the account Exchange permissions, but there you go, can’t have everything. Thanks again!

  33. Carol,

    No problem.

    The Exchange module shouldn’t be required locally on the GALSync Server as the commands are run remotely on the Exchange server.

    In order to get the remote Exchange powershell to work I needed to run a couple Exchange Management Shell commands on the Exchange server in the remote forest. These commands will need to be run for the GALSync service account in each forest on the appropriate Exchange server.

    These commands give the GALSync service account in each forest the appropriate permissions

    To add the GALSync service account to the Recipient Management Role Group:
    add-rolegroupmember “Recipient Management” -member GALSync

    To grant the service account permission to use PowerShell remotely on the remote forest Exchange server:
    set-user -identity “GALSYNC” -RemotePowerShellEnabled $True



  34. Carol,

    Windows Authentication also needs to be enabled and Anonymous Authentication needs to be disabled on the PowerShell virtual directory in IIS on the Exchange Servers.

    If you can combind this with the previous post that would be great

  35. I was able to tweak the modified GALSync Script to create X500 addresess for the contacts to mitigate potential issues with replies to old emails. I also changed the ADD section to be more like Carol’s original with the Alias and disabling the automatic application of address policy on the contact.

    Here’s my current ADDS section:

    ### ADDS

    foreach ($user in $colAddContact)
    write-host “ADDING contact for ” $user.mail

    $targetAddress = “SMTP:” + $user.mail

    $X500Address = “X500:” + $user.LegacyExchangeDN

    $alias = “c-” + $user.mail.split(“@”)[0]

    $hashAttribs = @{‘targetAddress’ = $targetAddress}
    $hashAttribs.add(“mailNickname”, $alias)

    foreach ($attrib in $arrAttribs)
    if ($user.$attrib -ne $null) { $hashAttribs.add($attrib, $user.$attrib) }
    ### Original AD Contact Creation
    ### New-ADObject -name $user.displayName -type contact -Path $targetOU -Description $user.description -server $targetDC -credential $targetCred -OtherAttributes $hashAttribs

    # Create Remote PowerShell Session
    $SO=New-PSSessionOption -SkipCACheck -SkipCNCheck –SkipRevocationCheck –ProxyAccessType None
    $PSSession1=New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $targetURI -Credential $targetCred -SessionOption $SO

    # Create Contact through the Remote PowerShell Session
    invoke-Command -Session $PSSession1 -ScriptBlock{param ($User,$alias,$targetOU,$targetaddress) New-MailContact -Name $User.displayname -Alias $alias -OrganizationalUnit $targetOU -ExternalEmailAddress $targetAddress -PrimarySmtpAddress $user.mail} -ArgumentList $User,$alias,$targetOU,$targetaddress

    # Add X500 Addressing
    $CurrentContact=invoke-Command -Session $PSSession1 -ScriptBlock{param ($User) Get-MailContact -identity $User.displayname} -ArgumentList $User
    $CurrentContact.EmailAddresses += $X500Address
    invoke-Command -Session $PSSession1 -ScriptBlock{param ($CurrentContact,$User) Set-MailContact -EmailAddresses $CurrentContact.EmailAddresses -identity $User.displayname} -ArgumentList $CurrentContact,$User

    # Close Remote PowerShell Session
    Remove-PSSession $PSSession1



  36. Hi Carol,

    The script is working just fantastic.

    When we enumerated 16000+ users it took 4 hours. Does the script behaviour remains same everytime it executes or it reads only delta changes and update the contacts which should reduce the executing time from 4 hours?

    If it does not update delta changes what is the work around.

    first time execution for 16000 user objects – took 4 hours
    second time only 100 existing user objects’ attributes changed – will it take 4 hours or less time?

    Rgds, Guruprasad.

  37. Wow so many users! I haven’t run it on such large numbers myself. The script will check through every contact the next time but should only update it if there’s a change to be made. I don’t know what the time difference will be but maybe you could report back.

  38. I’m having a tricky time creating the secure files..

    running “read-host -assecurestring | convertfrom-securestring | out-file C:\scripts\filename.txt”

    gets me this:

    ConvertFrom-SecureString : Cannot process argument because the value of argument “SecureString” is invalid. Change the value of the “SecureString” argument and run the operation again.
    At line:1 char:53
    + read-host -assecurestring | convertfrom-securestring <<<< | out-file C:\scripts\filename.txt
    + CategoryInfo : InvalidArgument: (:) [ConvertFrom-SecureString], PSArgumentException
    + FullyQualifiedErrorId : Argument,Microsoft.PowerShell.Commands.ConvertFr omSecureStringCommand

    Any assistance?

  39. Now I’ve got

    Get-ADObject : Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.
    At C:\ittools\GALSYNC.PS1:79 char:26
    + $colUsers = Get-ADObject <<<< -Filter $strSelectUsers -Properties * -Server $sourceDC -Credential $sourceCred
    + CategoryInfo : ResourceUnavailable: (:) [Get-ADObject], ADServe rDownException
    + FullyQualifiedErrorId : Unable to contact the server. This may be becaus e this server does not exist, it is currently down, or it does not have th e Active Directory Web Services running.,Microsoft.ActiveDirectory.Management.Commands.GetADObject

    No users found in source domain!

    But there is that service there.. Any clues?

  40. Make sure that you have everything working on the powershell side before trying to run the script. Run a powershell session as the user you will use to run the script, import the module (import-module ActiveDirectory), and then make sure you can run Get-ADObject correctly. Note also that the script uses the domain name to find a DC so DNS has to be working properly.

Comments are closed.