Mail Enabled Public Folders In A Hybrid Deployment: A Sad Story

 

The scenario that I will discuss today is not really a fancy one.

The story began with exchange on premise and a hybrid deployment where public folders are migrated to the Office365, 90% of users are migrated to the cloud.

The main question is how to make the mail enabled public folders appear in the on premise GAL and on the office365 GAL.

We already know that DirSync process won’t synchronize mail enabled public folders from the cloud to the on premise environment, that’s a fact.

The only options is to create a mail enabled contact in the on premise environment et voila Smile

Sounds quick easy and achievable BUT real life will carry more complicity with other staffs like distribution groups.

In our scenario all distribution Groups were created originally in the on premise and are synched to the cloud, let’s call them Cloud Groups.

The issue here is that you can’t add cloud folders directly to the group because the group was originally created in the on premise

If we try to add the mail contact to the on premise group, the contact will appear to the on premise users but will never appear as a member to the cloud users.

Ouch, what shall we do then?

I would think of two options however I won’t say I like any of them but those are what we have unfortunately:

1- Mirror the distribution Groups using a script and keep evaluating the membership.

2- Recreate all the distribution groups in the office365 as 90% of users are already in cloud.

In this article we will cover the second option unfortunately Smile

There are many things to cover here:

1- Handle email addresses + LegacyExchangeDN(X500)

2- Handle Multi Value attributes and convert them from  distinguishedName to Email address

2.a GrantSendOnBehalfTo

2.b ManagedBy

2.c ModeratedBy

2.d AcceptMessagesOnlyFromSendersOrMembers

2.e BypassModerationFromSendersOrMembers

2.f RejectMessagesFromSendersOrMembers

3- Handle SendAs and SendOnBehalf permissions

4- Handle group membership

5- Handle single value attributes that can be set in the Office365

They are defined in the script in the array $Listofsingleprops

I skipped some properties that can’t be changed in office365 like "MaxSendSize","MaxReceiveSize"

As the process is a bit complex, I decided to split it into several stages so that it’s also possible to use

the same process to recreate distribution groups in a different forest.

Stage 0: One time configuration

1- Create a new OU in active directory “NoDirSync”

2- Exclude this OU form the DirSync configuration

Stage1: Preparation

I developed a script to export all the desired informed to a csv file, the prepare-DGToOffice365.ps1 script.

1- This script will export the following attributes in addition to the group membership and the sendAs permission

$listOfprops=@("SamAccountName","EmailAddresses","ManagedBy","AcceptMessagesOnlyFrom","AcceptMessagesOnlyFromDLMembers",

"AcceptMessagesOnlyFromSendersOrMembers","BypassModerationFromSendersOrMembers",

"ModeratedBy","RejectMessagesFrom","RejectMessagesFromDLMembers",

"RejectMessagesFromSendersOrMembers","GrantSendOnBehalfTo","BypassNestedModerationEnabled",

"MemberJoinRestriction","MemberDepartRestriction","ReportToManagerEnabled",

"ReportToOriginatorEnabled","SendOofMessageToOriginatorEnabled","HiddenFromAddressListsEnabled",

"ModerationEnabled","RequireSenderAuthenticationEnabled","SendModerationNotifications","Alias",

"CustomAttribute1","CustomAttribute10","CustomAttribute11","CustomAttribute12",

"CustomAttribute13","CustomAttribute14","CustomAttribute15","CustomAttribute2",

"CustomAttribute3","CustomAttribute4","CustomAttribute5","CustomAttribute6","CustomAttribute7",

"CustomAttribute8","CustomAttribute9","DisplayName","MaxSendSize",

"MaxReceiveSize","PrimarySmtpAddress","SimpleDisplayName","MailTip")

<# 
.SYNOPSIS 
Use the Prepare-DGToOffice365.ps1 script to export the distribution groups information to csv file

.DESCRIPTION
    The ultimate objective is to delete all distribution groups from exchange on-premise and create them in the Office365 cloud.
    This sample script is the 1st script to export the distribution groups information from exchange on-premise
   

.NOTES 
    Author       :      Ahmed Ashour - ahmed.ashour@hotmail.fr
    Creation Date:         v1.0 4 Jan 2015
    Last Update  :      -----------------
#  DISCLAIMER:
# THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR
# A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL
# MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS,
# BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE
# SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION
# OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.

.PARAMETER GroupName
If the group name is not specified the script will export all the informations for all distribution groups

.PARAMETER OrganizationUnit
You need to specify the NoDirSyn Organization unit that on premise distribution groups will be temporary moved to in order to stop the dirsync for those groups

.PARAMETER Verbose
Enabled verbose logging

.Example

.\Prepare-DGToOffice365.ps1 -OrganizationUnit NoDirSync
This will export the information for all distribution group and move them all to the NoDirSync organization unit

.\Prepare-DGToOffice365.ps1 -GroupName "testDG01"
This will export the information for the group testdg01 and move it to the NoDirSync organization unit

.\Prepare-DGToOffice365.ps1 -GroupName "testDG01" -verbose
This will export the information for the group testdg01, move it to the NoDirSync organization unit and enable verbose logging

.OUTPUTS
Results will be saved to export.csv
Errors will be saved to errors.txt

#> 

[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[parameter(mandatory=$false)]$GroupName,
[parameter(mandatory=$True)]$OrganizationUnit
)
$ExportArray=@()
$ValueToStr=""
$DateStamp = Get-Date -f "hh:mm-dd-MM-yyyy"
import-module activedirectory
#Change the following line to search for all groups or a specific group
if($GroupName)
{
    $ListOfGroups=get-distributiongroup -identity $GroupName -ResultSize unlimited
}
else
{
   $ListOfGroups=get-distributiongroup -ResultSize unlimited
}

"-------------------------Start: Prepare-DGToOffice365 Script $($DateStamp)--------------------------------" | out-file -append errors.txt
if($ListOfGroups)
{
    Foreach($Group in $ListOfGroups)
    {
        "processing $($Group)"
   
        $listOfprops=@("SamAccountName","EmailAddresses","ManagedBy","AcceptMessagesOnlyFrom","AcceptMessagesOnlyFromDLMembers","AcceptMessagesOnlyFromSendersOrMembers","BypassModerationFromSendersOrMembers","ModeratedBy","RejectMessagesFrom","RejectMessagesFromDLMembers","RejectMessagesFromSendersOrMembers","GrantSendOnBehalfTo","BypassNestedModerationEnabled","MemberJoinRestriction","MemberDepartRestriction","ReportToManagerEnabled","ReportToOriginatorEnabled","SendOofMessageToOriginatorEnabled","HiddenFromAddressListsEnabled","ModerationEnabled","RequireSenderAuthenticationEnabled","SendModerationNotifications","Alias","CustomAttribute1","CustomAttribute10","CustomAttribute11","CustomAttribute12","CustomAttribute13","CustomAttribute14","CustomAttribute15","CustomAttribute2","CustomAttribute3","CustomAttribute4","CustomAttribute5","CustomAttribute6","CustomAttribute7","CustomAttribute8","CustomAttribute9","DisplayName","MaxSendSize","MaxReceiveSize","PrimarySmtpAddress","SimpleDisplayName","MailTip")
        $ListOfsingleprops=@("SamAccountName","BypassNestedModerationEnabled","MemberJoinRestriction","MemberDepartRestriction","ReportToManagerEnabled","ReportToOriginatorEnabled","SendOofMessageToOriginatorEnabled","Alias","CustomAttribute1","CustomAttribute10","CustomAttribute11","CustomAttribute12","CustomAttribute13","CustomAttribute14","CustomAttribute15","CustomAttribute2","CustomAttribute3","CustomAttribute4","CustomAttribute5","CustomAttribute6","CustomAttribute7","CustomAttribute8","CustomAttribute9","DisplayName","HiddenFromAddressListsEnabled","MaxSendSize","MaxReceiveSize","ModerationEnabled","PrimarySmtpAddress","RequireSenderAuthenticationEnabled","SimpleDisplayName","SendModerationNotifications","MailTip")
        $GroupToStr = New-Object PSObject
        $GroupToStr | Add-Member NoteProperty -Name "Name" -Value $Group.Name
   
        #1-Handle Members
        Write-Verbose "processing membership"
        $listOfMembers=(Get-DistributionGroupMember -Identity $Group.DisplayName | where{$_.recipienttype -eq "UserMailbox" -or $_.recipientType -eq "MailUniversalDistributionGroup"}) #Flash:only mailbox + Groups
        if($listOfMembers)
        {
            foreach($Member in $listOfMembers)
            {
                $ValueToStr=$ValueToStr+";"+(Get-recipient $Member.DistinguishedName).PrimarySmtpAddress.tostring()
                $ValueToStr=$ValueToStr.trimstart(";")
            }
        }
        else
        {
            write-warning "No members"
            $ValueToStr="Null"
        }
        $GroupToStr | Add-Member NoteProperty -Name "Members" -Value $ValueToStr

        ######################Handle Properties#################################
        Foreach($prop in $Listofprops)
        {

            write-verbose "processing $($prop)"
            $ValueToStr=""   

            if([string]::IsNullOrEmpty($group.$($prop)))
            {
                write-warning "Null value for $($prop)"
                $ValueToStr="Null"

            }
            else
            {

                Switch($prop)
                {
                    #2-Handle email addresses & X500
                    EmailAddresses {
                                        write-verbose "processing email addresses"
                                        $ProxyAddresses=$Group.EmailAddresses -split ";"
                                        $X500Address="X500:"+"$($Group.LegacyExchangeDN)"
                                        $ProxyAddresses+=$X500Address
                                        $ValueToStr=""
                                        Foreach($Address in $ProxyAddresses)
                                        {
                                            $ValueToStr=$ValueToStr+";"+$Address
                                            $ValueToStr=$ValueToStr.trimstart(";")
                                        }
                                    }

                    #3-Handle Single-Values
                    {$ListOfsingleprops -icontains $prop} {write-verbose "single prop $($prop)";$ValueToStr=$Group.$($prop)}

                    #4-Handle Multi-Values
                    Default {
                                write-verbose "Found Multi $($prop)"
                                $ValueToStr=""
                                foreach($AdMember in $Group.$($prop))
                                {
                                    $ValueToStr=$ValueToStr+";"+(Get-recipient $AdMember.DistinguishedName).PrimarySmtpAddress.tostring()
                                    $ValueToStr=$ValueToStr.trimstart(";")
                                }
                            }
                }   
  
            }

            #5-Populate the PSObject properties
            $GroupToStr | Add-Member NoteProperty -Name $prop -Value $ValueToStr
        }

        #6-Handle SendAs permission
        Write-Verbose "prcoessing sendAs permisisons"
        [String]$ValueToStr=""
        $ListOfPermissions=$group| Get-ADPermission | where{($_.Isinherited -eq $false) -and ($_.extendedrights -like "*Send-As*")}
        if($ListOfPermissions)
        {
            Foreach($Assignee in $ListOfPermissions)
            {
                $ValueToStr=$ValueToStr+";"+(Get-recipient $Assignee.User.ToString()).PrimarySmtpAddress.tostring()
            }
        $ValueToStr=$ValueToStr.trimstart(";")
        $GroupToStr | Add-Member NoteProperty -Name "sendAs" -Value $ValueToStr
        }
   
        #7-Add Group to the export array
        $ExportArray += $GroupToStr
        "Finsihed $($Group)"

        #8-Move the Group to the NoDirSync OU
        Write-Verbose "Moving the Group to the $($OrganizationUnit)"
        $NoDirSyncOU=Get-OrganizationalUnit $OrganizationUnit
        if($NoDirSyncOU)
        {
            Move-ADObject $Group.DistinguishedName -TargetPath $NoDirSyncOU.DistinguishedName
        }
        else
        {
            "$($Group)#Move#Couldn't find the NoDirSync $($OrganizationUnit)"| out-file -append errors.txt
        }
         "Finished processing $($Group.Name)"
        "--------------------------------------------------------"  
    }
#8-export all groups to csv file
$ExportArray|export-csv export.csv
}
"--------------------Finish: Prepare-DGToOffice365 Script ---------------------------------------------------" | out-file -append errors.txt

2- If you didn’t specify a group name when you run the script, this script will search for all distribution groups

3- After the script completes, the group will be moved to the NoDirSync OU.

4- The script will save any errors to errors.txt file

Stage2: Validation:

1- Run DirSync process

2- Check Office365 and make sure that the group is no more available in Office365

Stage3: Creation

1- From Azure PowerShell module, you can run the second script .\Create-DGToOffice365.ps1 –verbose

<# 
.SYNOPSIS 
Use the Create-DGToOffice365.ps1 script to create the distribution groups in Office365 based on a csv file

.DESCRIPTION
    The ultimate objective is to delete all distribution groups from exchange on-premise and create them in the Office365 cloud.
    This sample script is the 1st script to create the distribution groups was previously removed from exchange on premise into Office365.
   

.NOTES 
    Author       :      Ahmed Ashour - ahmed.ashour@hotmail.fr
    Creation Date:         v1.0 4 Jan 2015
    Last Update  :      ----------------- 
#  DISCLAIMER:
# THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR
# A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL
# MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS,
# BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE
# SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION
# OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.

.PARAMETER Verbose
Enabled verbose logging

.Example

.\Create-DGToOffice365.ps1
This will create the distribution groups based on the csv file.

.\Create-DGToOffice365.ps1 -verbose
This will create the distribution groupsbased on the csv file and enable verbose logging

.Inputs
Script will require a csv file with all the attributes
Name    Members    SamAccountName    EmailAddresses    ManagedBy    AcceptMessagesOnlyFrom    AcceptMessagesOnlyFromDLMembers    AcceptMessagesOnlyFromSendersOrMembers    BypassModerationFromSendersOrMembers    ModeratedBy    RejectMessagesFrom    RejectMessagesFromDLMembers    RejectMessagesFromSendersOrMembers    GrantSendOnBehalfTo    BypassNestedModerationEnabled    MemberJoinRestriction    MemberDepartRestriction    ReportToManagerEnabled    ReportToOriginatorEnabled    SendOofMessageToOriginatorEnabled    HiddenFromAddressListsEnabled    ModerationEnabled    RequireSenderAuthenticationEnabled    SendModerationNotifications    Alias    CustomAttribute1    CustomAttribute10    CustomAttribute11    CustomAttribute12    CustomAttribute13    CustomAttribute14    CustomAttribute15    CustomAttribute2    CustomAttribute3    CustomAttribute4    CustomAttribute5    CustomAttribute6    CustomAttribute7    CustomAttribute8    CustomAttribute9    DisplayName    MaxSendSize    MaxReceiveSize    PrimarySmtpAddress    SimpleDisplayName    MailTip    sendAs

.OUTPUTS
Errors will be saved to errors.txt
#> 

[CmdletBinding(SupportsShouldProcess=$true)]
Param()

#Function to open the import csv file
Function Get-FileName($initialDirectory)
{  
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") |
Out-Null

$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = "All files (*.*)| *.*"
$OpenFileDialog.ShowDialog() | Out-Null
return $OpenFileDialog.filename
}
$ImportFile=Get-FileName -initialDirectory "c:\"

#Function to connect to Office365
Function New-O365ExchangeSession()
{
param([parameter(mandatory=$true)]$365master)

#close any old remote session
Get-PSSession | Remove-PSSession -Confirm:$false

#start a new office 365 remote session
$365session = New-PSSession -ConfigurationName "Microsoft.Exchange" -ConnectionUri 'https://ps.outlook.com/powershell' -Credential $365master -Authentication Basic -AllowRedirection
$office365 = Import-PSSession $365session -AllowClobber

}
$365master = get-credential ashour@myelephantdev.onmicrosoft.com
New-O365ExchangeSession $365master

Function DoYExist()
{
param(
[array]$RecordList,
$Operation,
$ParentGroup
)
    $ValidValueToStr=@()
    Foreach($Record in $RecordList)
    {
        if(Get-Recipient $Record | where{$_.RecipientType -eq "UserMailbox" -or $_.RecipientType -eq "MailUniversalDistributionGroup"})
        {
            write-verbose "Found $($Record)"
            $ValidValueToStr+=$Record  
        }
        else
        {
            Write-Verbose "wrong user$($Record)"
            "$($ParentGroup)#$($Operation)#$($Record)#invalid User or Group" | out-file -append errors.txt
        }
    }
    return ,$ValidValueToStr
}

#Load CSV File
$DateStamp = Get-Date -f "dd-MM-yyyy"
$ListOfGroups=import-csv $ImportFile
"-------------------------Start: Create-DGToOffice365 Script $($DateStamp)--------------------------------" | out-file -append errors.txt
if($ListOfGroups)
{
    ForEach($Group in $ListOfGroups)
    {
        "Processing $($Group.name)"
        If(Get-DistributionGroup $Group.Name)
        {
            #1a-Group is available, exit
            Write-Verbose "$($Group.Name) already exist"
        }
        else
        {
            write-verbose "Creating Group $($Group.Name)"
            #1-Create the Group
            $Error.Clear()
            New-DistributionGroup -Name $Group.Name -DisplayName $Group.displayName -PrimarySmtpAddress $Group.PrimarySmtpAddress -Alias $Group.alias
            If ($Error) {"$($Group.displayName)#Creation#$($Error[0].Exception)" | out-file -Append errors.txt}
        }

        "Finished processing $($Group.Name)"
        "--------------------------------------------------------"  
    }
}
else
{
    "No entries in the csv file" | out-file -append errors.txt
}
"--------------------Finish: Create-DGToOffice365 Script---------------------------------------------------" | out-file -append errors.txt

2- The script will ask you to provide the csv file will be used to create the distribution groups in office365

Stage4:Update

1- After all the distribution groups are created in Office365, it’s now possible to update all the properties that have group to group relations

2- From Azure PowerShell module, you can run the script .\Update-DGToOffice365.ps1 –verbose

<# 
.SYNOPSIS 
Use the Update-DGToOffice365.ps1 script to update the distribution groups attributes from a csv file

.DESCRIPTION
    The ultimate objective is to delete all distribution groups from exchange on-premise and create them in the Office365 cloud.
    This sample script is the 3rd script to update the distribution groups created on Office365 with the old information exported from exchange on-premise
   

.NOTES 
    Author       :      Ahmed Ashour - ahmed.ashour@hotmail.fr
    Creation Date:         v1.0 4 Jan 2015
    Last Update  :      -----------------

#  DISCLAIMER:
# THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR
# A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL
# MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS,
# BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE
# SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION
# OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.

.PARAMETER Verbose
Enabled verbose logging

.Example

.\update-DGToOffice365.ps1
This will set all the distribution groups properties based on the csv file.

.\update-DGToOffice365.ps1 -verbose
This will set all the distribution groups properties based on the csv file and enable verbose logging

.Inputs
Script will require a csv file with all the attributes
Name    Members    SamAccountName    EmailAddresses    ManagedBy    AcceptMessagesOnlyFrom    AcceptMessagesOnlyFromDLMembers    AcceptMessagesOnlyFromSendersOrMembers    BypassModerationFromSendersOrMembers    ModeratedBy    RejectMessagesFrom    RejectMessagesFromDLMembers    RejectMessagesFromSendersOrMembers    GrantSendOnBehalfTo    BypassNestedModerationEnabled    MemberJoinRestriction    MemberDepartRestriction    ReportToManagerEnabled    ReportToOriginatorEnabled    SendOofMessageToOriginatorEnabled    HiddenFromAddressListsEnabled    ModerationEnabled    RequireSenderAuthenticationEnabled    SendModerationNotifications    Alias    CustomAttribute1    CustomAttribute10    CustomAttribute11    CustomAttribute12    CustomAttribute13    CustomAttribute14    CustomAttribute15    CustomAttribute2    CustomAttribute3    CustomAttribute4    CustomAttribute5    CustomAttribute6    CustomAttribute7    CustomAttribute8    CustomAttribute9    DisplayName    MaxSendSize    MaxReceiveSize    PrimarySmtpAddress    SimpleDisplayName    MailTip    sendAs

.OUTPUTS
Errors will be saved to errors.txt
#> 

[CmdletBinding(SupportsShouldProcess=$true)]
Param()

#Function to open the import csv file
Function Get-FileName($initialDirectory)
{  
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") |
Out-Null

$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = "All files (*.*)| *.*"
$OpenFileDialog.ShowDialog() | Out-Null
return $OpenFileDialog.filename
}
$ImportFile=Get-FileName -initialDirectory "c:\"

#Function to connect to Office365
Function New-O365ExchangeSession()
{
param([parameter(mandatory=$true)]$365master)

    #close any old remote session
    Get-PSSession | Remove-PSSession -Confirm:$false

    #start a new office 365 remote session
    $365session = New-PSSession -ConfigurationName "Microsoft.Exchange" -ConnectionUri 'https://ps.outlook.com/powershell' -Credential $365master -Authentication Basic -AllowRedirection
    $office365 = Import-PSSession $365session -AllowClobber
}
$365master = get-credential #ashour@myelephantdev.onmicrosoft.com
New-O365ExchangeSession $365master

#Function DoIExist
Function DoYExist()
{
param(
[array]$RecordList,
$Operation,
[String]$ParentGroup,
[Parameter(Mandatory=$True)][string]$ImportFile
)
    $ValidValueToStr=@()
    Foreach($Record in $RecordList)
    {
        if(Get-Recipient $Record -erroraction "silentlycontinue"| where{$_.RecipientType -eq "UserMailbox" -or $_.RecipientType -eq "MailUniversalDistributionGroup"})
        {
            write-verbose "Found $($Record)"
            $ValidValueToStr+=$Record  
        }
        else
        {
            Write-Verbose "wrong user$($Record)"
            "$($ParentGroup)#$($Operation)#$($Record)#invalid User or Group" | out-file -append errors.txt
        }
    }
    return ,$ValidValueToStr
}

#Load CSV File
$ValueToArray=""
$DateStamp = Get-Date -f "hh:mm-dd-MM-yyyy"
$ListOfGroups=import-csv $ImportFile
"-------------------------Start: Update-DGToOffice365 Script $($DateStamp)--------------------------------" | out-file -append errors.txt
if($ListOfGroups)
{
    ForEach($Group in $ListOfGroups)
    {
        "Processing $($Group.name)"
        If(Get-DistributionGroup $Group.Name -erroraction silentlycontinue)
        {
            Write-Verbose "Found $($Group.Name)"
       
            #1-Add X500 and smtp addresses
            write-verbose "Email addresses $($Group.Name)"
            $ValueToArray=@($Group.EmailAddresses.split(";"))
            $Error.Clear()
            Set-DistributionGroup -Identity $Group.Name  -EmailAddresses $ValueToArray
            If ($Error) {"$($Group.displayName)#Email Addresses#$($Error[0].Exception)" | out-file -Append errors.txt}
               
            #2-Processing single value attribues
            Write-Verbose "Processing Single Value attributes"       
            $ListOfSplat=@{}
            #"MaxSendSize","MaxReceiveSize" are valid for onpremise but are not available for office365 set-distributingroup command
            $Listofsingleprops=@("BypassNestedModerationEnabled","MemberJoinRestriction","MemberDepartRestriction","ReportToManagerEnabled","ReportToOriginatorEnabled","SendOofMessageToOriginatorEnabled","HiddenFromAddressListsEnabled","ModerationEnabled","RequireSenderAuthenticationEnabled","SendModerationNotifications","CustomAttribute1","CustomAttribute10","CustomAttribute11","CustomAttribute12","CustomAttribute13","CustomAttribute14","CustomAttribute15","CustomAttribute2","CustomAttribute3","CustomAttribute4","CustomAttribute5","CustomAttribute6","CustomAttribute7","CustomAttribute8","CustomAttribute9","DisplayName","Alias","PrimarySmtpAddress","SimpleDisplayName","MailTip")
            Foreach($Kei in $Listofsingleprops)
            {
                if($Group.$($kei) -eq "Null")
                {
                    write-verbose "Skip $($kei)"
                }
                else
                {
                    Switch($Group.$($kei))
                    {
                        True {$Kval=$True}
                        False {$Kval=$False}
                        Default{$Kval=$Group.$($kei)}
                    }
                    $ListOfSplat.Add($kei,$Kval)         
                }
            }

            $Error.Clear()
            Set-distributiongroup -identity $Group.Name @ListOfSplat
            If ($Error) {"$($Group.displayName)#SingleProps#$($Error[0].Exception)" | out-file -Append errors.txt}

            #3- Processing MultiValue attributes "GrantSendOnBehalfTo","ManagedBy","ModeratedBy","AcceptMessagesOnlyFromSendersOrMembers","BypassModerationFromSendersOrMembers","RejectMessagesFromSendersOrMembers"
            Write-Verbose "Processing Multi Value attributes"
            $ListOfMultiProps=@("GrantSendOnBehalfTo","ManagedBy","ModeratedBy","AcceptMessagesOnlyFromSendersOrMembers","BypassModerationFromSendersOrMembers","RejectMessagesFromSendersOrMembers")
            Foreach($Prop in $ListOfMultiProps)
            {
                Write-Verbose "processing $($prop)"
                $ValueToArray=$Group.$($prop).split(";") | unique
                $ValidValueToStr=(DoYExist $ValueToArray $prop $($Group.displayName))
                if($ValidValueToStr)
                {
                    $HashSplat=@{}
                    $HashSplat.Add($prop,$ValidValueToStr)
                    $Error.Clear()
                    Set-DistributionGroup -Identity $($group.name) @HashSplat
                    If ($Error) {"$($Group.displayName)#$($prop)#$($Error[0].Exception)" | out-file -Append errors.txt}
                }
            }
       
            #4-Process MemberOf
            write-verbose "Processing MemberOf $($Group.Name)"
            if(!$Group.Members -eq "Null")
            {
                $ValueToArray=$Group.members.split(";") | unique
                $ValidValueToStr=(DoYExist $ValueToArray "Members" $($Group.displayName))
                Foreach($GroupMember in $ValidValueToStr)
                {
                    write-verbose "adding the member $($GroupMember)"
                    $Error.Clear()
                    Add-DistributionGroupMember -Identity $Group.Name -Member $GroupMember
                    If ($Error) {"$($Group.displayName)#Memberof#$($Error[0].Exception)" | out-file -Append errors.txt}
                }
            }
            #4-Process sendAs permission
            write-verbose "Processing the sendAs $($Group.Name)"
            $ListOfTrustee=$Group.SendAs.split(";") | unique
            If($ListOfTrustee)
            {
                Foreach($Trustee in $ListOfTrustee)
                {
                    #Cloud only Add-RecipientPermission $Group.Name -AccessRights SendAs -Trustee $Trustee -Confirm:$false
                    #On-Premise only Add-Adpermission $Group.Name -ExtendedRights Send-As -user $Trustee -Confirm:$false
                    $Error.Clear()
                    Write-Verbose "processing $($Trustee)"
                    Add-RecipientPermission $Group.Name -AccessRights SendAs -Trustee $Trustee -Confirm:$false
                    If ($Error) {"$($Group.displayName)#SendAs#$($Trustee)#$($Error[0].Exception)" | out-file -Append errors.txt}
                }
            }
        }
        else
        {
            Write-Verbose "$($Group.Name) doesn't exist"
            "$($Group.displayName)#Search for the Group#Doesn't Exist" | out-file -Append errors.txt
        }
        "Finished processing $($Group.Name)"
        "--------------------------------------------------------"  
    }
}
else
{
    "No entries in the csv file" | out-file -append errors.txt
}
"--------------------Finish: update-DGToOffice365 Script ---------------------------------------------------" | out-file -append errors.txt

3- The script will ask you to provide the csv file will be used to create the distribution group in office365

4- The script will save any errors to errors.txt file

Stage5:Cleanup

Once you feel everything is working fine you can connect to active directory and remove all groups in the NoDirSync OU.

Enjoy Smile

# THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR
# A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL
# MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS,
# BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE
# SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION
# OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.