Sync-GalToMutipleTenants.ps1

<#PSScriptInfo
    .VERSION 1.1
    .GUID 09adead3-5a12-4461-92b5-6b0adf8fc50e
    .AUTHOR SHAHIN BASHEER - shahinbasheer@hotmail.com
    .COMPANYNAME TECHNICALLY POSSIBLE
    .COPYRIGHT https://technicallypossible.com
    .TAGS GalSync ContactSync OneToManyHYBRID ExchangeOnline
    .LICENSEURI https://technicallypossible.com
    .PROJECTURI https://technicallypossible.com
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES ExchangeOnlineManagement / Onprem Exchange Shell
    .REQUIREDSCRIPTS
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES
        FullSync, GroupSync & UpdateGuestUser included
    .DESCRIPTION
        SINGLE EXCHANGE ONPREMISE EXCHANGE TO MULTIPLE O365 TENANT CONTACT SYNC
        Script reads recipients from each OU and create contacts in the respective tenants.
    Each OU should uniquely identify a department & domain
        Address Lists should be precreated in the tenant
        # GUIDE #
            https://technicallypossible.com
        # SCRIPT PATH #
            Script should be copied to C:\POWERSHELL\Sync-GalToMultipleTenants folder. Path can be changed using $ScriptPath variable
        # FILES #
            Script Requires 2 CSV files (Tenants.csv & OUSyncMaps.csv) in the \Configs Subfolder
            Tenants.csv # Sample File
                Tenant,AppID,CertThumbPrint
                FirstTenant.onmicrosoft.com,abcdefghi-1234-5678-abcd-123456784321,ABCD699BA1E5A5E12347B4B12ABCD7CC01FABCD
                SecondTenant.onmicrosoft.com,cdefghiab-1234-5678-cdef-023456784320,ABCD699BA1E5A5E12347B4B12ABCD7CC01FABCD
            OuSyncMaps.csv # Sample File
                DeptCode,ADOU,ParentTenant,GlobalHide
                DEPT1_CODE,OU=Sync,OU=First Department,OU=HOSTING,DC=techposs,DC=internal,FirstTenant.onmicrosoft.com
                DEPT2_CODE,OU=Sync,OU=Second Department,OU=HOSTING,DC=techposs,DC=internal,SecondTenant.onmicrosoft.com
    # MODULES #
            OnPremise Exchange Management Shell should be instlledin the server
            Latest ExchangeOnlineManagement Modules should be installed in the Server
    # CERTS #
            Certificate should be installed in the user store/
    # PARAMETERS #
            -FullSync - Compares each contact in the tenant./ use -FullSync Switch
            -NoGroupSync - Do not create contact for each group object / use -NoGroupSync Switch
            -UpdateGuestUser - Update the mail users came from Azure AD guest user / use -UpdateGuestUser switch
#>


[CmdletBinding()]
    param (
        [switch]
        $FullSync,
        [switch] 
        $NoGroupSync,
        [switch] 
        $UpdateGuestUser
)

function Get-Now ()
{
    $now = (Get-Date).DateTime
    Return ($now).ToString()
}
function Add-ExContact {
    param (
        [Parameter(Mandatory)]
        $Contact
    )
    try
    {
        $Unique = Get-Random -Minimum 10000 -Maximum 99999
        $Name = ""
        $Name = $Contact.DisplayName + "_" + $Unique
        if ($Name.Length -gt 62)
        {
            $Name = $Name.Substring(0,62)
        }
        New-MailContact -Name $Name -DisplayName $Contact.DisplayName -Alias ($Contact.PrimarySmtpAddress.local).tostring() -ExternalEmailAddress ($Contact.PrimarySmtpAddress).tostring() -ErrorAction Stop 
        Start-Sleep -Seconds 0.5
        if ( $Contact.Type -notlike "*Group")
        {
            Get-Contact ($Contact.PrimarySmtpAddress).tostring() | Set-Contact -Title $Contact.Title -Company $Contact.Company -Department $Contact.Department -Office $Contact.Office -Phone $Contact.Phone 
        }
        $X500 = $null
        $X500 = "X500:" + $Contact.LegacyExchangeDN
        Get-MailContact ($Contact.PrimarySmtpAddress).tostring() | Set-MailContact -CustomAttribute12 $Contact.OU -CustomAttribute13 $Contact.Type -CustomAttribute14 $TimeStamp -CustomAttribute15 $DeptCode -Emailaddresses @{Add=$X500}
        (Get-Now) + ": Created NEW Contact :: " + $Contact.PrimarySmtpAddress | Out-File $TenantLogFile -Append
    }
    catch
    {
        (Get-Now) + ": Failed to Create or Update Contact :: " + $Contact.PrimarySmtpAddress + "`r`n" + $Error[0] | Out-File $TenantLogFile -Append
    }
}
function Update-ExContact {
    param (
        [Parameter(Mandatory)]
        $Contact
    )
    try
    {
        if ( $Contact.Type -notlike "*Group")
        {
            Get-Contact ($Contact.PrimarySmtpAddress).tostring() | Set-Contact -Title $Contact.Title -Company $Contact.Company -Department $Contact.Department -Office $Contact.Office -Phone $Contact.Phone 
        }
        #Start-Sleep -Seconds 0.5
        $X500 = $null
        $X500 = "X500:" + $Contact.LegacyExchangeDN
        Get-MailContact ($Contact.PrimarySmtpAddress).tostring() | Set-MailContact -DisplayName $Contact.DisplayName -CustomAttribute12 $Contact.OU -CustomAttribute13 $Contact.Type -CustomAttribute14 $TimeStamp -CustomAttribute15 $DeptCode -Emailaddresses @{Add=$X500} 
        (Get-Now) + ": Updated the Contact :: " + $Contact.PrimarySmtpAddress | Out-File $TenantLogFile -Append
    }
    catch
    {
        (Get-Now) + ": Failed to Update Contact :: " + $Contact.PrimarySmtpAddress + "`r`n" + $Error[0] | Out-File $TenantLogFile -Append
    }
}
function Remove-ExContact {
    param (
        [Parameter(Mandatory)]
        $Contact
    )
    try
    {
        Remove-MailContact $Contact -Confirm:$false  -ErrorAction Stop
        (Get-Now) + ": Removed Contact :: " + $Contact | Out-File $TenantLogFile -Append
    }
    catch
    {
        (Get-Now) + ": Failed to Remove Contact :: " + $Contact + "`r`n" + $Error[0] | Out-File $TenantLogFile -Append
    }
}

########################### SET THE SCRIPT PATH #####################################

$ScriptPath = "C:\SCHEDULER\Sync-GalToMultipleTenants"  
Set-Location $ScriptPath

########################### BEGIN CODE #####################################

#$Rand = Get-Random -min 1000000000
$TimeStamp = Get-Now

########################### READ THE TENANTS INFO FILE #####################################

if ( $null -eq (Get-Item "$ScriptPath\Configs" -ErrorAction SilentlyContinue))
{
    New-Item -Path $ScriptPath -Name "Configs" -ItemType "Directory" -ErrorAction SilentlyContinue
    New-Item -Path "$ScriptPath\Configs\" -Name "CONFIG.txt" -ItemType "file" -Value "Tenants.CSV and OUSyncMaps.CSV Should be Placed in this Folder" -ErrorAction SilentlyContinue
}
$Year =  ( Get-Date ).ToString('yyyy')
$Name = ( Get-Date ).ToString('MM') + " " + ( Get-Date ).ToString('MMMM')
$LogPath ="$ScriptPath" + "\Logs\" + $Year + "\" + $Name
if ( (Test-Path $LogPath) -eq $false )
{ 
    New-Item -ItemType Directory -Path $Logpath -ErrorAction Stop
}
$Name = ( Get-Date ).ToString('yyMMdd-HHmmss-fff-')
$LogFile = "$Logpath\" + $Name + "io.log"
$ConnectLogFile = "$LogPath\" + $Name + "connections.log"
$ReadOULogFile = "$LogPath\" + $Name + "ou_processing.log"
try 
{
    $Tenants = $null
    $Tenants = Import-Csv "$ScriptPath\Configs\Tenants.csv" -ErrorAction Stop
}
catch
{
     (Get-Now) + "`r`nError Reading TENANT INFORMATION File`r`n"  + $Error[0] + "`r`n" | Out-File $LogFile -Append
     Exit
}

########################### READ THE TENANTS/OU SYNC MAP FILE #####################################
try 
{
    $SyncMaps = $null
    $SyncMaps = Import-Csv "$ScriptPath\Configs\OUSyncMaps.csv" -ErrorAction Stop
}
catch
{
    (Get-Now) + "`r`nError Reading SYNC MAP File`r`n" + $Error[0] + "`r`n" | Out-File $LogFile -Append 
    Exit
}
########################### LOAD THE EXCHANGE PS MODULES #####################################
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
try 
{
    Add-PSSnapin Microsoft.Exchange.Management.PowerShell* -ErrorAction Stop
}
catch 
{                
    (Get-Now) + "`r`nError Loading Exchange OnPremise Management Shell. `r`n" + $Error[0] + "`r`n" | Out-File $ConnectLogFile -Append
    Exit
} 

########################### BEGIN FOREACH - READ THE MAILOXES OF EACH OU :: SyncMaps.csv ###################################

$Index = 0
foreach ( $SyncMapRow in $SyncMaps )
{
    ########################### SKIP OUs SET AS GLOBAL HIDE #####################################
    try
    {
        $Progress  = (($Index + 1) / $SyncMaps.Count) * 100
        Set-Variable "OuContacts$Index" -Value $null
    }
    catch 
    {
        $Progress  = $Index + 1
    }
    Write-Progress -Activity "Recipient Import from AD" -Status ("Processing " + ($Index+1) + " of " + ($SyncMaps.Count) + ": OU : " + $SyncMapRow.ADOU) -PercentComplete  $Progress
    if ( $SyncMapRow.GlobalHide -eq "TRUE" )
    {
        (Get-Now) + ": Skipping the Department OU :: Department set to Global Hide : " + $SyncMapRow.ADOU + "`r`n" | Out-File $ReadOULogFile -Append 
        $Index++
        Continue;
    }

    ########################### READ FROM OU FOR MAILBOXES #####################################
    (Get-Now) + ": BEGIN Reading Recipient information from the Department OU : " + $SyncMapRow.ADOU | Out-File $ReadOULogFile -Append  
    try 
    {
        $OuMailboxes = $null
        $OuMailboxes = Get-Mailbox -ResultSize Unlimited -OrganizationalUnit $SyncMapRow.ADOU -ErrorAction Stop | Where-Object {$_.HiddenFromAddressListsEnabled -eq $false} | Select-Object DisplayName,PrimarySmtpAddress,LegacyExchangeDN,UserPrincipalName,RecipientTypeDetails,OrganizationalUnit | Sort-Object PrimarySmtpAddress 
        #$OuMailboxes = Get-Mailbox -ResultSize Unlimited -OrganizationalUnit $SyncMapRow.ADOU -ErrorAction Stop | Where-Object {$_.HiddenFromAddressListsEnabled -eq $false} | Select-Object DisplayName,UserPrincipalName,SamAccountName,PrimarySmtpAddress,CustomAttribute10,CustomAttribute11,CustomAttribute12,CustomAttribute13,CustomAttribute14,OrganizationalUnit,Database,LegacyExchangeDN | Sort-Object PrimarySmtpAddress
        $OuContacts = $null
        $OuContacts = New-Object System.Collections.ArrayList
        $OuMailboxes | Foreach-Object{
            [String]$Upn = ""
            [String]$Upn = $_.UserPrincipalName
            Write-Verbose "Extracting Recipient Information $Upn" 
            $MbxUsr = $null
            #$AdUsr = $null
            if ( $Upn -eq "" -or $null -eq $Upn)
            {
                (Get-Now) + ": Error:: Invalid UserPrincipalName. Skipping the Mailbox : " + $_.DisplayName + " " + $_.PrimarySmtpAddress | Out-File $ReadOULogFile -Append
            }
            else 
            {
                $Mbxusr = Get-User $Upn | Select-Object FirstName,LastName,Title,Department,Company,Office,Phone,Manager
                #$AdUsr = Get-AdUser -Filter { UserprincipalName -eq $Upn } -Properties *
            }
            $ContactEntry = $null
            if ( $null -eq $MbxUsr )
            {
                $ContactEntry = [PSCustomObject]@{
                    DisplayName = $_.DisplayName
                    PrimarySmtpAddress = $_.PrimarySmtpAddress
                    LegacyExchangeDN = $_.LegacyExchangeDN
                    Type = $_.RecipientTypeDetails
                    OU = $_.OrganizationalUnit
                    FirstName = ""
                    LastName = ""
                    Title = ""
                    Department = ""
                    Company = ""
                    Office = ""
                    Phone = ""
                }
            }    
            else 
            {
                $ContactEntry = [PSCustomObject]@{
                    DisplayName = $_.DisplayName
                    PrimarySmtpAddress = $_.PrimarySmtpAddress
                    LegacyExchangeDN = $_.LegacyExchangeDN
                    Type = $_.RecipientTypeDetails
                    OU = $_.OrganizationalUnit
                    #Name = $_.DisplayName
                    #UserPrincipalName = $_.UserPrincipalName
                    #SamAccountName = $_.SamAccountName
                    #CustomAttribute10 = $_.CustomAttribute10
                    #CustomAttribute11 = $_.CustomAttribute11
                    #CustomAttribute12 = $_.CustomAttribute12
                    #CustomAttribute13 = $_.CustomAttribute13
                    #CustomAttribute14 = $_.CustomAttribute14
                    FirstName = $MbxUsr.FirstName
                    LastName = $MbxUsr.LastName
                    Title = $MbxUsr.Title
                    Department = $MbxUsr.Department
                    Company = $MbxUsr.Company
                    Office = $MbxUsr.Office
                    Phone = $MbxUsr.Phone
                    #Manager = $MbxUsr.Manager
    
                    #$Section = $AdUsr.Division
                    #OfficePhone = $AdUsr.OfficePhone
                    #IPPhone = $AdUsr.IPPhone
                    #OtherPhone = $AdUsr.otherTelephone
                    #EmpID = $AdUsr.EmployeeID
                    #EmpNo = $AdUsr.EmployeeNumber
                    #Description = $AdUsr.Description
                    #Manager = $AdUsr.Manager
                }
            }
            $OuContacts.Add($ContactEntry) | Out-Null
        }
        if ( ($OuMailboxes -is [System.Array]) -or ($null -eq $OuMailboxes) )
        {
            (Get-Now) + ": Total Number of Mailboxes from OU : " + $OuMailboxes.count | Out-File $ReadOULogFile -Append             
        }
        else 
        {
            (Get-Now) + ": Total Number of Mailboxes from OU : 1 " | Out-File $ReadOULogFile -Append
        }
        $OuMailboxes = $null
        $OuMailboxes = Get-RemoteMailbox -ResultSize Unlimited -OnPremisesOrganizationalUnit $SyncMapRow.ADOU -ErrorAction Stop | Where-Object {$_.HiddenFromAddressListsEnabled -eq $false} | Select-Object DisplayName,PrimarySmtpAddress,LegacyExchangeDN,UserPrincipalName,RecipientTypeDetails,OnPremisesOrganizationalUnit | Sort-Object PrimarySmtpAddress 
        $OuMailboxes | Foreach-Object{
            [String]$Upn = ""
            [String]$Upn = $_.UserPrincipalName
            Write-Verbose "Extracting Recipient Information $Upn" 
            $MbxUsr = $null
            #$AdUsr = $null
            if ( $Upn -eq "" -or $null -eq $Upn)
            {
                (Get-Now) + " Error: Invalid UserPrincipalName. Skipping the Remote Mailbox : " + $_.DisplayName + " " + $_.PrimarySmtpAddress | Out-File $ReadOULogFile -Append
            }
            else 
            {
                $Mbxusr = Get-User $Upn | Select-Object FirstName,LastName,Title,Department,Company,Office,Phone,Manager
                #$AdUsr = Get-AdUser -Filter { UserprincipalName -eq $Upn } -Properties *
            }
            $ContactEntry = $null
            if ( $null -eq $MbxUsr )
            {
                $ContactEntry = [PSCustomObject]@{
                    DisplayName = $_.DisplayName
                    PrimarySmtpAddress = $_.PrimarySmtpAddress
                    LegacyExchangeDN = $_.LegacyExchangeDN
                    Type = $_.RecipientTypeDetails
                    OU = $_.OnPremisesOrganizationalUnit
                    FirstName = ""
                    LastName = ""
                    Title = ""
                    Department = ""
                    Company = ""
                    Office = ""
                    Phone = ""
                }
            }    
            else 
            {
                $ContactEntry = [PSCustomObject]@{
                    DisplayName = $_.DisplayName
                    PrimarySmtpAddress = $_.PrimarySmtpAddress
                    LegacyExchangeDN = $_.LegacyExchangeDN
                    Type = $_.RecipientTypeDetails
                    OU = $_.OnPremisesOrganizationalUnit
                    FirstName = $MbxUsr.FirstName
                    LastName = $MbxUsr.LastName
                    Title = $MbxUsr.Title
                    Department = $MbxUsr.Department
                    Company = $MbxUsr.Company
                    Office = $MbxUsr.Office
                    Phone = $MbxUsr.Phone
                }
            }
           $OuContacts.Add($ContactEntry) | Out-Null
        }
        if ( ($OuMailboxes -is [System.Array]) -or ($null -eq $OuMailboxes) )
        {
            (Get-Now) + ": Total Number of Remote Mailboxes from OU : " + $OuMailboxes.count | Out-File $ReadOULogFile -Append
        }
        else 
        {
            (Get-Now) + ": Total Number of Remote Mailboxes from OU : 1 " | Out-File $ReadOULogFile -Append
        }
        if (!$NoGroupSync)
        {
            $OuGroups = $null
            $OuGroups = Get-DistributionGroup -ResultSize Unlimited -OrganizationalUnit $SyncMapRow.ADOU -ErrorAction Stop | Where-Object { ($_.RecipientTypeDetails -ne "RoomList") -and ($_.HiddenFromAddressListsEnabled -eq $false) } | Select-Object DisplayName,PrimarySmtpAddress,LegacyExchangeDN,RecipientTypeDetails,OrganizationalUnit | Sort-Object PrimarySmtpAddress 
            $OuGroups | Foreach-Object{
                Write-Verbose "Extracting Recipient Information $_.PrimarySMTPAddress" 
                $ContactEntry = $null
                $ContactEntry = [PSCustomObject]@{
                    DisplayName = $_.DisplayName
                    PrimarySmtpAddress = $_.PrimarySmtpAddress
                    LegacyExchangeDN = $_.LegacyExchangeDN
                    Type = $_.RecipientTypeDetails
                    OU = $_.OrganizationalUnit
                    FirstName = ""
                    LastName = ""
                    Title = ""
                    Department = ""
                    Company = ""
                    Office = ""
                    Phone = ""
                }
               $OuContacts.Add($ContactEntry) | Out-Null
            }
            if ( ($OuGroups -is [System.Array]) -or ($null -eq $OuGroups) )
            {
                (Get-Now) + ": Total Number of Groups from OU : " + $OuGroups.count | Out-File $ReadOULogFile -Append
            }
            else 
            {
                (Get-Now) + ": Total Number of Groups from OU : 1 " | Out-File $ReadOULogFile -Append
            }
        }
    }
    catch 
    {
        (Get-Now) + "`r`nError Reading Recipient Information from OU `r`n" + $Error[0] | Out-File $ReadOULogFile -Append
    }
    finally
    {
        if ($null -eq $OuContacts) 
        {
            Set-Variable "OuContacts$Index" -Value $null
            (Get-Now) + ": Recipients from OU will be skipped: "| Out-File $ReadOULogFile -Append                      
        }
        else
        {
            Set-Variable "OuContacts$Index" -Value $OuContacts   
            (Get-Now) + ": Total Number of Recipients from OU : " + $OuContacts.count | Out-File $ReadOULogFile -Append           
            #(Get-Now) + ": Number of Recipients from OU : " + $OuMailboxes.count | Out-File $ReadOULogFile -Append
        }
    }
    (Get-Now) + ": END Reading Recipient information from the Department OU `r`n" | Out-File $ReadOULogFile -Append  
    Remove-Variable OuMailboxes -ErrorAction SilentlyContinue
    Remove-Variable OuGroups -ErrorAction SilentlyContinue
    Remove-Variable OuContacts -ErrorAction SilentlyContinue
    $Index++
    #Start-Sleep -Seconds .25
} 
Remove-PSSnapin Microsoft.Exchange.Management.PowerShell* -ErrorAction SilentlyContinue
########################### END FOREACH - READ THE MAILOXES OF EACH OU :: SyncMaps.csv ###################################
########################### LOAD THE EXCHANGE ONLINE PS MODULE #####################################
try 
{
    Import-Module ExchangeOnlineManagement -ErrorAction Stop
}
catch 
{                
    (Get-Now) + "`r`nError Loading Exchange PS Module V2 `r`n" + $Error[0] + "`r`n" | Out-File $ConnectLogFile -Append
    Exit
} 
########################### BEGIN FOREACH - READ EACH TENANT & APPID INFO :: Tenants.csv #####################################
$TenantIndex = 0
foreach ( $Tenant in $Tenants )
{
    try
    {
        $Progress  = (($TenantIndex + 1) / $Tenants.Count) * 100
    }
    catch 
    {
        $Progress  = $TenantIndex + 1
    }
   
    $ExclusionFileName = $null
    $ExclusionFileName = "$ScriptPath\Configs\" + $Tenant.Tenant + ".csv"
    $ExclusionFile = $null
    $ExclusionFile = Get-Item $ExclusionFileName -ErrorAction SilentlyContinue
    $ExclusionList = $null

    $TenantLogFile = $null
    $TenantLogFile = "$LogPath\" + $Name + $Tenant.Tenant + ".log"

    Write-Progress -Activity ("GAL Sync to Tenant " + ($TenantIndex+1) + " of " + ($Tenants.Count) + " : " + $Tenant.Tenant) -Status ("Verifying OUs for Sync" ) -PercentComplete  $Progress
    "`r`n" + (Get-Now) + "`r`n[ BEGIN Processing Tenant :: " + $Tenant.Tenant + " ]" | Out-File $TenantLogFile -Append 
    if ($FullSync)
    {
        "[ Full Sync ] is Set :: All Contacts will be compared for changes. Changes will be updated" | Out-File $TenantLogFile -Append
    }
    else 
    {
        "[ Delta Sync ] is Set :: Existing Contacts in the Tenant will be skipped" | Out-File $TenantLogFile -Append
    }
    if ($NoGroupSync)
    {
        "[ No Group Sync ] is Set :: All Existing Group Contacts will be removed from the Tenant !" | Out-File $TenantLogFile -Append
    }
    else 
    {
        "[ Group Sync ] is Set :: Contacts will be Created/Updated for Groups" | Out-File $TenantLogFile -Append
    }
    if ($UpdateGuestUser)
    {
        "[ Update Guest User ] is Set :: All Existing Guest Users proxy address will be cleared and New Contacts will be created !!`r`n" | Out-File $TenantLogFile -Append
    }
    else 
    {
        "[ Update Guest User ] is Not Set :: All Existing Guest Users will not be modified in the Tenant `r`n" | Out-File $TenantLogFile -Append
    }

    if ( $null -ne $ExclusionFile )
    {
        $ExclusionList = Import-Csv $ExclusionFile -ErrorAction SilentlyContinue
    }
    ########################### BEGIN FOREACH - SYNC CONTACTS TO EACH TENAT FROM OUs IN THE SYNC MAP FILE :: SyncMaps.csv #####################################
    $IsSessionActive = $false    
    $Index = 0
    foreach ( $SyncMapRow in $SyncMaps )
    {
        ########################### SKIP OUs OF THE SAME TENANT #####################################
        if ( $SyncMapRow.ParentTenant -eq $Tenant.Tenant )
        {
            (Get-Now) + ": Skipping the Department OU :: Department is the Target Tenant : " + $SyncMapRow.ADOU + "`r`n"| Out-File $TenantLogFile -Append 
            $Index++
            Continue;
        }

        ########################### SKIP OUs SET AS GLOBAL HIDE #####################################
        if ( $SyncMapRow.GlobalHide -eq "TRUE" )
        {
            (Get-Now) + ": Skipping the Department OU :: Department set to Global Hide : " + $SyncMapRow.ADOU + "`r`n"| Out-File $TenantLogFile -Append 
            $Index++
            Continue;
        }

        ########################### SKIP OUs Defined IN TENANT EXCLUSION FILE #####################################
        $IsExcluded = $false
        if ( $null -ne $ExclusionList )
        {
            ########################### BEGIN LOOP - EACH OU IN THE EXCLUSION FILE #####################################
            foreach ($Exclusion in $ExclusionList)
            {
                if ( $SyncMapRow.ADOU -eq $Exclusion.ADOU )
                {
                    $IsExcluded = $true
                    Break;
                }                
            }         
            ########################### BEGIN LOOP - EACH OU IN THE EXCLUSION FILE #####################################
        }
        if ( $IsExcluded )
        {
            (Get-Now) + ": Skipping the Department OU :: Department in Tenant Exclusion List : " + $SyncMapRow.ADOU + "`r`n"| Out-File $TenantLogFile -Append 
            $Index++
            Continue;
        }

        ########################### PROCESS THE OU CONTACT SYNC TO TENANT #####################################
        try
        {
            $Progress  = (($Index + 1) / $SyncMaps.Count) * 100
        }
        catch 
        {
            $Progress  = $Index + 1
        }
        Write-Progress -Activity ("GAL Sync to Tenant " + ($TenantIndex+1) + " of " + ($Tenants.Count) + " : " + $Tenant.Tenant) -Status ("Syncing OU " + ($Index+1) + " of " + ($SyncMaps.Count) + ": OU : " + $SyncMapRow.ADOU) -PercentComplete  $Progress
        $DeptCode = $null
        $DeptCode = $SyncMapRow.DeptCode 
              
        if ( !$IsSessionActive )
        {
            ############################ CONNECT TO THE CURRENT TENANT - ONLY ONCE PER TENANT #####################
            try 
            {
                ########################### CERT with the Thumbprint SHOULD BE IN USER STORE ##########################
                if ( ($null -eq $Tenant.Tenant) -or ("" -eq $Tenant.Tenant) )
                {
                    Throw "Missing Tenant Name : $Tenant"
                }
                if ( ($null -eq $Tenant.CertThumbPrint) -or ("" -eq $Tenant.CertThumbPrint) )
                {
                    Throw "Missing CertThumbprint for Tenant: $Tenant"
                }
                if ( ($null -eq $Tenant.AppID) -or ("" -eq $Tenant.AppID) )
                {
                    Throw "Missing AppID for Tenant: $Tenant"
                }
                Connect-ExchangeOnline -CertificateThumbPrint $Tenant.CertThumbPrint -Organization $Tenant.Tenant -AppID $Tenant.AppID -ShowBanner:$false
                $IsSessionActive = $true
                (Get-Now) + "`r`nSuccessfully Connected to TENANT "+ $Tenant.Tenant + "`r`n" | Out-File $ConnectLogFile -Append
            }
            catch 
            {                
                (Get-Now) + "`r`nError Connecting to TENANT "+ $Tenant.Tenant + "`r`n" + $Error[0] + "`r`n"| Out-File $ConnectLogFile -Append
                (Get-Now) + ": ERROR Processing the TENANT - Terminate : `r`n" | Out-File $TenantLogFile -Append  
                $IsSessionActive = $false
                Break
            }
            Write-Verbose "Session Status : $IsSessionActive"
            #Check if ignore Existing users are Set
            if ($UpdateGuestUser) 
            {
                (Get-Now) + ": =BEGIN Reading Existing Guest Users from the Tenant : " + $DeptCode | Out-File $TenantLogFile -Append  
                try 
                {
                    #Reading Existing Azure AD Guest User Information (ONCE Per Tenant) into Collecion than the default Fixed Size onject array
                    $TenantGuestUsers = $null
                    [System.Collections.ArrayList]$TenantGuestUsers = @( Get-MailUser -ResultSize Unlimited  -ErrorAction Stop | Where-Object {$_.RecipientTypeDetails -eq "GuestMailUser" -and $_.PrimarySMTPAddress -ne ""} | Select-Object DisplayName, PrimarySmtpAddress | Sort-Object PrimarySmtpAddress )
                    #check If Guest Users are empty
                    if ($null -eq $TenantGuestUsers) 
                    {
                        (Get-Now) + ": No Guest Users in the Tenant with Proper Proxy Address : " | Out-File $TenantLogFile -Append
                    }
                    else
                    {
                        (Get-Now) + ": Number of Guest Users in the Tenant with Proper Proxy Address : " + $TenantGuestUsers.count | Out-File $TenantLogFile -Append           
                    }
                }
                catch 
                {
                    (Get-Now) + ": Error Reading Existing Guest User Information from Tenant. Update Guest User is Set to False`r`n" + $Error[0] | Out-File $TenantLogFile -Append
                    $UpdateGuestUser = $false
                }
                (Get-Now) + ": =END Reading Guest Users from the Tenant ::::::::: `r`n" | Out-File $TenantLogFile -Append  
            }
        }
        (Get-Now) + ": BEGIN Processing the Department OU with Dept Code $DeptCode : " + $SyncMapRow.ADOU + "`r`n"| Out-File $TenantLogFile -Append  
        if ($IsSessionActive)
        {
            ############################ BEGIN READ THE CONTACTS OF THE DEPT FROM TENANT #####################
            (Get-Now) + ": -BEGIN Reading Existing Contacts from the Tenant With Dept Code : " + $DeptCode | Out-File $TenantLogFile -Append  
            #Read Dept Contacts from Tenant
            #Read Full Contact Information if Full Sync is Set
            if ($FullSync)
            {
                try 
                {
                    #Reading Existing Contacts into Collecion than the default Fixed Size onject array
                    $TenantBaseContacts = $null
                    $TenantBaseContacts = Get-MailContact -ResultSize Unlimited  -ErrorAction Stop | Where-Object {$_.CustomAttribute15 -eq $DeptCode} | Select-Object DisplayName, PrimarySmtpAddress, CustomAttribute12, CustomAttribute13 | Sort-Object PrimarySmtpAddress
                    $TenantContacts = $null
                    $TenantContacts = New-Object System.Collections.ArrayList
                    $TenantBaseContacts | Foreach-Object{
                        [String]$ID = ""
                        [String]$ID = $_.PrimarySmtpAddress
                        Write-Verbose "Extracting Recipient Information $ID" 
                        $TContact = $null
                        if ( $ID -eq "" -or $null -eq $ID)
                        {
                            (Get-Now) + ": Error:: Invalid PrimarySmtpAddress. Skipping the Contact : " + $_.DisplayName + " " + $_.PrimarySmtpAddress | Out-File $TenantLogFile -Append
                        }
                        else 
                        {
                            $TContact = Get-Contact $ID | Select-Object FirstName,LastName,Title,Department,Company,Office,Phone
                        }
                        $TenantContactEntry = $null
                        if ( $null -eq $TContact )
                        {
                            $TenantContactEntry = [PSCustomObject]@{
                                DisplayName = $_.DisplayName
                                PrimarySmtpAddress = $_.PrimarySmtpAddress
                                Type = $_.CustomAttribute13
                                OU = $_.CustomAttribute12
                                FirstName = ""
                                LastName = ""
                                Title = ""
                                Department = ""
                                Company = ""
                                Office = ""
                                Phone = ""
                            }
                        }    
                        else 
                        {
                            $TenantContactEntry = [PSCustomObject]@{
                                DisplayName = $_.DisplayName
                                PrimarySmtpAddress = $_.PrimarySmtpAddress
                                Type = $_.CustomAttribute13
                                OU = $_.CustomAttribute12
                                FirstName = $TContact.FirstName
                                LastName = $TContact.LastName
                                Title = $TContact.Title
                                Department = $TContact.Department
                                Company = $TContact.Company
                                Office = $TContact.Office
                                Phone = $TContact.Phone
                            }
                        }
                        $TenantContacts.Add($TenantContactEntry) | Out-Null
                    }
                }
                catch 
                {
                    (Get-Now) + ": Error Reading Existing Contacts from the Tenant with Dept Code $DeptCode `r`n" + $Error[0] | Out-File $TenantLogFile -Append
                }
            }
            #Read Base Contact Information if Full Sync is Not Set
            else 
            {
                try 
                {
                    #Reading Existing Contacts into Collecion than the default Fixed Size onject array
                    $TenantContacts = $null
                    #Early Filter did not work with variable in the filter block
                    #[System.Collections.ArrayList]$TenantContacts = Get-MailContact -ResultSize Unlimited -Filter { CustomAttribute15 -eq $DeptCode} -ErrorAction Stop| Select-Object DisplayName, PrimarySmtpAddress | Sort-Object PrimarySmtpAddress
                    [System.Collections.ArrayList]$TenantContacts = @( Get-MailContact -ResultSize Unlimited  -ErrorAction Stop | Where-Object {$_.CustomAttribute15 -eq $DeptCode} | Select-Object DisplayName, PrimarySmtpAddress | Sort-Object PrimarySmtpAddress ) 
                }
                catch 
                {
                    (Get-Now) + ": Error Reading Existing Contacts from the Tenant with Dept Code $DeptCode `r`n" + $Error[0] | Out-File $TenantLogFile -Append
                }
            }
            #check If Contacts are empty
            if ($null -eq $TenantContacts) 
            {
                (Get-Now) + ": No Contacts in the Tenant with DeptCode $DeptCode : " | Out-File $TenantLogFile -Append
            }
            else
            {
                (Get-Now) + ": Number of Contacts in the Tenant with Dept Code $DeptCode : " + $TenantContacts.count | Out-File $TenantLogFile -Append           
            }
            (Get-Now) + ": -END Reading Existing Contacts from the Tenant ::::::::: `r`n" | Out-File $TenantLogFile -Append  
            ############################ END READ THE CONTACTS OF THE DEPT FROM TENANT #####################
            
            ############################ BEGIN SYNCING OF THE DEPT CONTACTS TO TENANT #####################
            (Get-Now) + ": --BEGIN Syncing Contacts from the Department OU to Tenant With Dept Code : " + $DeptCode | Out-File $TenantLogFile -Append  
            $OuContacts = $null
            $OuContacts = (Get-Variable -Name OuContacts$Index -ErrorAction SilentlyContinue).value 
            #If Recipients from OU are EMPTY :: No Sync
            if ($null -eq $OuContacts) 
            {
                (Get-Now) + ": No Contacts from the Department OU. Check $ReadOuLogFile for more details. Contacts with DeptCode $DeptCode will be skipped: "| Out-File $TenantLogFile -Append    
            }
            #If Recipients from OU are NOT EMPTY :: Check for existing Contacts in Tenant
            else
            {
                (Get-Now) + ": Number of Contacts from Department OU with Dept Code $DeptCode : " + $OuContacts.count | Out-File $TenantLogFile -Append
                (Get-Now) + ": Number of Contacts Existing in the Tenant with Dept Code $DeptCode : " + $TenantContacts.count | Out-File $TenantLogFile -Append   
                                   
                #If Recipients are read from OU and there are NO Contacts in Tenant :: Create All Contacts
                if ($null -eq $TenantContacts)
                {
                    (Get-Now) + ": First Time Contacts Provisioning Triggered with $DeptCode : Update Guest User is Set to False"  | Out-File $TenantLogFile -Append   
                    foreach ($OuContact in $OuContacts )
                    {
                        Add-ExContact($OuContact)
                    }                     
                }
                #If Recipients are read from OU and there are existing Contacts in Tenant :: Create/Update/Remove
                else 
                {
                    foreach ($OuContact in $OuContacts)
                    {
                        $IsSynced = $false
                        $i = 0
                        foreach ($TenantContact in $TenantContacts)
                        {
                            Write-Verbose "Verifying the Sync Status of the Contact : $OuContact.PrimarySmtpAddress"
                            if ($TenantContact.PrimarySmtpAddress -eq ($OuContact.PrimarySmtpAddress).toString())
                            {
                                $IsSynced = $true
                                #If FullSync Specified :: Compare Each Existing Contact
                                if ($FullSync) 
                                {
                                    if ( ($TenantContact.DisplayName -ne $OuContact.DisplayName) -or 
                                    ($TenantContact.OU -ne $OuContact.OU) -or 
                                    ($TenantContact.Type -ne $OuContact.Type) -or
                                    ($TenantContact.Title -ne $OuContact.Title) -or 
                                    ($TenantContact.Department -ne $OuContact.Department) -or
                                    ($TenantContact.Company -ne $OuContact.Company) -or 
                                    ($TenantContact.Office -ne $OuContact.Office) -or
                                    ($TenantContact.Phone -ne $OuContact.Phone) )
                                    {
                                        Write-Verbose "Updating Existing Contact $OuContact.PrimarySmtpAddress"
                                        Update-ExContact($OuContact)
                                    }
                                    else 
                                    {
                                        Write-Verbose "Skipping Contact, No Changes : $TenantContact.PrimarySmtpAddress"
                                        (Get-Now) + ": Skipped the Contact, No Changes :: " + $OuContact.PrimarySmtpAddress | Out-File $TenantLogFile -Append                                       
                                    }
                                }
                                #If FullSync Not Specified :: Skip the Existing Contact
                                else 
                                {
                                    Write-Verbose "Skipping Existing Contact : $TenantContact.PrimarySmtpAddress"
                                    (Get-Now) + ": Skipped the Existing Contact :: " + $OuContact.PrimarySmtpAddress | Out-File $TenantLogFile -Append
                                }
                                $TenantContacts.RemoveAt($i)
                                break;
                            }
                            $i++
                        }
                        # Contact Not in Tenant
                        if (!$IsSynced)
                        {   
                            # Check If Update Guest User is Specified : Update the proxy address of the Existing Mail User
                            if ($UpdateGuestUser)
                            {
                                $i = 0
                                foreach ($TenantGuestUser in $TenantGuestUsers)
                                {
                                    Write-Verbose "Verifying the Contact $OuContact.PrimarySmtpAddress is a Guest Mail User"
                                    if ($TenantGuestUser.PrimarySmtpAddress -eq ($OuContact.PrimarySmtpAddress).toString())
                                    {
                                        try
                                        {
                                            $x = $null
                                            $x = ($OuContact.PrimarySmtpAddress).tostring()
                                            Set-MailUser $x -EmailAddresses $null -ErrorAction Stop
                                            (Get-Now) + ": The Email Addresss " + $OuContact.PrimarySmtpAddress + " is a Guest User. Cleared the proxy address" | Out-File $TenantLogFile -Append
                                            Start-Sleep -Seconds 15
                                        }
                                        catch
                                        {
                                            (Get-Now) + ": Error Clearing Proxy Address of the mail user. " + $OuContact.PrimarySmtpAddress + "`r`n" + $Error[0] | Out-File $TenantLogFile -Append                                            
                                        }
                                        $TenantGuestUsers.RemoveAt($i)
                                        break;
                                    }
                                    $i++
                                }
                            }
                            # Create New Contact
                            Add-ExContact($OuContact)
                        }    
                    }
                }
                #If Tenant Contact Collection is NOT Empty :: Remove the Contacts, Since the Recipient is Removed from EX OnPremise
                if ($TenantContacts.Count -ne 0)
                {
                    foreach ($TenantContact in $TenantContacts)
                    {
                        Remove-ExContact ($TenantContact.PrimarySmtpAddress)
                    }
                }
            }
            (Get-Now) + ": --END Syncing Contacts to Tenant ::::::::: `r`n" | Out-File $TenantLogFile -Append  
            ############################ END SYNCING OF THE DEPT CONTACTS TO TENANT #####################
            Remove-Variable OuContacts -ErrorAction SilentlyContinue
            Remove-Variable TenantContacts -ErrorAction SilentlyContinue
            Remove-Variable TenantBaseContacts -ErrorAction SilentlyContinue
        }
        Write-Verbose "OU $Index : $SyncMapRow.ADOU"
        (Get-Now) + ": END Processing the Department OU with Dept Code $DeptCode : `r`n" | Out-File $TenantLogFile -Append  
        #Start-Sleep -Seconds 1
        $Index++
    }
    Remove-Variable TenantGuestUsers -ErrorAction SilentlyContinue 
    if ( $IsSessionActive )
    {
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
        (Get-Now) + "`r`nDisonnected from TENANT "+ $Tenant.Tenant + "`r`n" | Out-File $ConnectLogFile -Append
    }
    Write-Verbose "$TenantIndex Completed OUs for Tenant $Tenant.Tenant"
    "`r`n" + (Get-Now) + "`r`n[ END Processing Tenant :: " + $Tenant.Tenant + " ]" | Out-File $TenantLogFile -Append 
    #Start-Sleep -Seconds 1
    $TenantIndex++
    ########################### END FOREACH - SYNC CONTACTS TO EACH TENAT FROM OUS IN THE SYNC MAP FILE :: SyncMaps.csv #####################################
}
########################### END FOREACH - READ EACH TENANT & APPID INFO :: Tenants.csv #####################################

for ( $i=0;$i -lt $SyncMaps.count; $i++)
{
    Remove-Variable OuContacts$i
}
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
Remove-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue
$FullSync = $null
$NoGroupSync = $null
$UpdateGuestUser = $null
########################### END CODE #####################################