O365Synchronizer.psm1

function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Start-TimeLog { 
    <#
    .SYNOPSIS
    Starts a new stopwatch for logging time.
 
    .DESCRIPTION
    This function starts a new stopwatch that can be used for logging time durations.
 
    .EXAMPLE
    Start-TimeLog
    Starts a new stopwatch for logging time.
 
    #>

    [CmdletBinding()]
    param()
    [System.Diagnostics.Stopwatch]::StartNew()
}
function Stop-TimeLog { 
    <#
    .SYNOPSIS
    Stops the stopwatch and returns the elapsed time in a specified format.
 
    .DESCRIPTION
    The Stop-TimeLog function stops the provided stopwatch and returns the elapsed time in a specified format. The function can output the elapsed time as a single string or an array of days, hours, minutes, seconds, and milliseconds.
 
    .PARAMETER Time
    Specifies the stopwatch object to stop and retrieve the elapsed time from.
 
    .PARAMETER Option
    Specifies the format in which the elapsed time should be returned. Valid values are 'OneLiner' (default) or 'Array'.
 
    .PARAMETER Continue
    Indicates whether the stopwatch should continue running after retrieving the elapsed time.
 
    .EXAMPLE
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    # Perform some operations
    Stop-TimeLog -Time $stopwatch
    # Output: "0 days, 0 hours, 0 minutes, 5 seconds, 123 milliseconds"
 
    .EXAMPLE
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    # Perform some operations
    Stop-TimeLog -Time $stopwatch -Option Array
    # Output: ["0 days", "0 hours", "0 minutes", "5 seconds", "123 milliseconds"]
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time,
        [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner',
        [switch] $Continue
    )
    Begin {
    }
    Process {
        if ($Option -eq 'Array') {
            $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds"
        } else {
            $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        }
    }
    End {
        if (-not $Continue) {
            $Time.Stop()
        }
        return $TimeToExecute
    }
}
function Write-Color { 
    <#
    .SYNOPSIS
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    .DESCRIPTION
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    It provides:
    - Easy manipulation of colors,
    - Logging output to file (log)
    - Nice formatting options out of the box.
    - Ability to use aliases for parameters
 
    .PARAMETER Text
    Text to display on screen and write to log file if specified.
    Accepts an array of strings.
 
    .PARAMETER Color
    Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER BackGroundColor
    Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER StartTab
    Number of tabs to add before text. Default is 0.
 
    .PARAMETER LinesBefore
    Number of empty lines before text. Default is 0.
 
    .PARAMETER LinesAfter
    Number of empty lines after text. Default is 0.
 
    .PARAMETER StartSpaces
    Number of spaces to add before text. Default is 0.
 
    .PARAMETER LogFile
    Path to log file. If not specified no log file will be created.
 
    .PARAMETER DateTimeFormat
    Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss
 
    .PARAMETER LogTime
    If set to $true it will add time to log file. Default is $true.
 
    .PARAMETER LogRetry
    Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2.
 
    .PARAMETER Encoding
    Encoding of the log file. Default is Unicode.
 
    .PARAMETER ShowTime
    Switch to add time to console output. Default is not set.
 
    .PARAMETER NoNewLine
    Switch to not add new line at the end of the output. Default is not set.
 
    .PARAMETER NoConsoleOutput
    Switch to not output to console. Default all output goes to console.
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    Write-Color -t "my text" -c yellow -b green
    Write-Color -text "my text" -c red
 
    .EXAMPLE
    Write-Color -Text "Testuję czy się ładnie zapisze, czy będą problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput
 
    .NOTES
    Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
    Project support: https://github.com/EvotecIT/PSWriteColor
    Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } 
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } 
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {

                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                }
            } else {
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline 
                    }
                }
            }
        }
        if ($NoNewLine -eq $true) {
            Write-Host -NoNewline 
        } else {
            Write-Host 
        } 
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  
    }
    if ($Text.Count -and $LogFile) {

        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) {
            $TextToFile += $Text[$i]
        }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) {
                    "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                } else {
                    "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                $Saved = $true
            } catch {
                if ($Saved -eq $false -and $Retry -eq $LogRetry) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                } else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function Compare-UserToContact {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER UserID
    Identity of the user to synchronize contacts to. It can be UserID or UserPrincipalName
 
    .PARAMETER ExistingContactGAL
    User/Contact object from GAL
 
    .PARAMETER Contact
    Existing contact in user's personal contacts
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $UserID,
        [PSCustomObject] $ExistingContactGAL,
        [PSCustomObject] $Contact
    )
    $AddressProperties = 'City', 'State', 'Street', 'PostalCode', 'Country'
    if ($Contact.PSObject.Properties.Name -contains 'MailNickName') {
        $TranslatedContact = $Contact
    } elseif ($Contact.PSObject.Properties.Name -contains 'Nickname') {

        $TranslatedContact = [ordered] @{}
        foreach ($Property in $Script:MappingContactToUser.Keys) {
            if ($Property -eq 'Mail') {
                $TranslatedContact[$Property] = $Contact.EmailAddresses | ForEach-Object { $_.Address }
            } elseif ($Script:MappingContactToUser[$Property] -like "*.*") {
                $TranslatedContact[$Property] = $Contact.$($Script:MappingContactToUser[$Property].Split('.')[0]).$($Script:MappingContactToUser[$Property].Split('.')[1])
            } else {
                $TranslatedContact[$Property] = $Contact.$($Script:MappingContactToUser[$Property])
            }
        }
    } else {
        throw "Compare-UserToContact - Unknown user object $($ExistingContactGAL.PSObject.Properties.Name)"
    }

    $SkippedProperties = [System.Collections.Generic.List[string]]::new()
    $UpdateProperties = [System.Collections.Generic.List[string]]::new()
    foreach ($Property in $Script:MappingContactToUser.Keys) {
        if ([string]::IsNullOrEmpty($ExistingContactGAL.$Property) -and [string]::IsNullOrEmpty($TranslatedContact.$Property)) {
            $SkippedProperties.Add($Property)
        } else {
            if ($ExistingContactGAL.$Property -ne $TranslatedContact.$Property) {
                Write-Verbose -Message "Compare-UserToContact - Property $($Property) for $($ExistingContactGAL.DisplayName) / $($ExistingContactGAL.Mail) different ($($ExistingContactGAL.$Property) vs $($Contact.$Property))"
                if ($Property -in $AddressProperties) {
                    foreach ($Address in $AddressProperties) {
                        if ($UpdatedProperties -notcontains $Address) {
                            $UpdateProperties.Add($Address)
                        }
                    }
                } else {
                    $UpdateProperties.Add($Property)
                }

            } else {
                $SkippedProperties.Add($Property)
            }
        }
    }
    [PSCustomObject] @{
        UserId      = $UserId
        Action      = 'Update'
        DisplayName = $ExistingContactGAL.DisplayName
        Mail        = $ExistingContactGAL.Mail
        Update      = $UpdateProperties | Sort-Object -Unique
        Skip        = $SkippedProperties | Sort-Object -Unique
        Details     = ''
        Error       = ''
    }
}

function Convert-ConfigurationToSettings {
    [CmdletBinding()]
    param(
        [scriptblock] $ConfigurationBlock
    )
    $Configuration = & $ConfigurationBlock
    foreach ($C in $ConfigurationBlock) {

    }
}
function Convert-GraphObjectToContact {
    [cmdletbinding()]
    param(
        $SourceObject
    )

    $MappingMailContact = [ordered] @{
        DisplayName               = 'DisplayName'
        Name                      = 'DisplayName'
        PrimarySmtpAddress        = 'Mail'
        CustomAttribute1          = 'CustomAttribute1'
        CustomAttribute2          = 'CustomAttribute2'
        ExtensionCustomAttribute1 = 'ExtensionCustomAttribute1'

    }
    $MappingContact = [ordered] @{
        DisplayName         = 'DisplayName'
        Name                = 'DisplayName'
        WindowsEmailAddress = 'Mail'
        Title               = 'JobTitle'
        FirstName           = 'GivenName'
        LastName            = 'SurName'
        HomePhone           = 'HomePhone'
        MobilePhone         = 'MobilePhone'
        Phone               = 'BusinessPhones'
        CompanyName         = 'CompanyName'
        Department          = 'Department'
        Office              = 'Office'
        StreetAddress       = 'StreetAddress'
        City                = 'City'
        StateOrProvince     = 'StateOrProvince'
        PostalCode          = 'PostalCode'
        CountryOrRegion     = 'CountryOrRegion'

    }

    $NewContact = [ordered] @{}
    foreach ($Property in $MappingContact.Keys) {
        $PropertyName = $MappingContact[$Property]
        if ($PropertyName -eq 'BusinessPhones') {
            $NewContact[$Property] = [string] $SourceObject.$PropertyName
        } else {
            $NewContact[$Property] = $SourceObject.$PropertyName
        }
    }
    $NewMailContact = [ordered] @{}
    foreach ($Property in $MappingMailContact.Keys) {
        $PropertyName = $MappingMailContact[$Property]

        $NewMailContact[$Property] = $SourceObject.$PropertyName

    }
    $Output = [ordered] @{
        Contact     = [PSCustomObject] $NewContact
        MailContact = [PSCustomObject] $NewMailContact
    }
    $Output
}
function Get-O365ContactsFromTenant {
    [cmdletbinding()]
    param(
        [Array] $Domains
    )
    $CurrentContactsCache = [ordered]@{}
    Write-Color -Text "[>] ", "Getting current contacts" -Color Yellow, White, Cyan
    try {
        $CurrentContacts = Get-Contact -ResultSize Unlimited -ErrorAction Stop
    } catch {
        Write-Color -Text "[e] ", "Failed to get current contacts. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Yellow, White, Red
        return
    }

    Write-Color -Text "[>] ", "Getting current mail contacts (improving dataset)" -Color Yellow, White, Cyan
    try {
        $CurrentMailContacts = Get-MailContact -ResultSize Unlimited -ErrorAction Stop
    } catch {
        Write-Color -Text "[e] ", "Failed to get current contacts. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Yellow, White, Red
        return
    }

    Write-Color -Text "[i] ", "Preparing ", $CurrentContacts.Count, " (", "Mail contacts: ", $CurrentMailContacts.Count , ")", " contacts for comparison" -Color Yellow, White, Cyan, White, white, Cyan, White, Yellow

    foreach ($Contact in $CurrentMailContacts) {
        $Found = $false
        foreach ($Domain in $Domains) {
            if ($Contact.PrimarySmtpAddress -notlike "*@$Domain") {
                continue
            } else {
                $Found = $true
            }
        }
        if ($Found) {
            $CurrentContactsCache[$Contact.PrimarySmtpAddress] = [ordered] @{
                MailContact = $Contact
                Contact     = $null
            }
        }
    }

    foreach ($Contact in $CurrentContacts) {
        if ($CurrentContactsCache[$Contact.WindowsEmailAddress]) {
            $CurrentContactsCache[$Contact.WindowsEmailAddress].Contact = $Contact
        } else {

        }
    }
    $CurrentContactsCache
}
function Get-O365ExistingMembers {
    [cmdletbinding()]
    param(
        [scriptblock] $UserProvidedFilter,
        [string[]] $MemberTypes,
        [switch] $RequireAccountEnabled,
        [switch] $RequireAssignedLicenses
    )

    if ($UserProvidedFilter) {
        try {
            $FilterInformation = & $UserProvidedFilter
        } catch {
            Write-Color -Text "[e] ", "Failed to execute user provided filter because of error in line ", $_.InvocationInfo.ScriptLineNumber, " with message: ", $_.Exception.Message -Color Yellow, White, Red
            return $false
        }
    } else {
        $FilterInformation = @()
    }
    $GroupIDs = [ordered] @{}
    $GroupIDsExclude = [ordered] @{}
    $PropertyFilter = [ordered] @{}
    $PropertyFilterExclude = [ordered] @{}
    foreach ($Filter in $FilterInformation) {
        if ($Filter.FilterType -eq 'Group') {
            if ($Filter.Type -eq 'Include') {
                foreach ($GroupID in $Filter.GroupID) {
                    $GroupIDs[$GroupID] = $true
                }
            } elseif ($Filter.Type -eq 'Exclude') {
                foreach ($GroupID in $Filter.GroupID) {
                    $GroupIDsExclude[$GroupID] = $true
                }
            }
        } elseif ($Filter.FilterType -eq 'Property') {
            if ($Filter.Type -eq 'Include') {
                $PropertyFilter[$Filter.Property] = $Filter
            } elseif ($Filter.Type -eq 'Exclude') {
                $PropertyFilterExclude[$Filter.Property] = $Filter
            }
        } else {
            Write-Color -Text "[e] ", "Unknown filter type: $($Filter.FilterType)" -Color Red, White
            return $false
        }
    }

    $ExistingUsers = [ordered] @{}
    if ($MemberTypes -contains 'Member' -or $MemberTypes -contains 'Guest') {
        try {
            $getMgUserSplat = @{
                Property    = $Script:PropertiesUsers
                All         = $true
                ErrorAction = 'Stop'
            }
            if ($GroupIDs.Keys.Count -gt 0) {
                $getMgUserSplat.ExpandProperty = 'memberOf'
            }
            $Users = Get-MgUser @getMgUserSplat
        } catch {
            Write-Color -Text "[e] ", "Failed to get users. ", "Error: $($_.Exception.Message)" -Color Red, White, Red
            return $false
        }
        :NextUser foreach ($User in $Users) {

            if ($RequireAccountEnabled) {
                if (-not $User.AccountEnabled) {
                    Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by account is disabled"
                    continue
                }
            }
            if ($RequireAssignedLicenses) {
                if ($User.AssignedLicenses.Count -eq 0) {
                    Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by no assigned licenses"
                    continue
                }
            }
            if ($GroupIDs.Keys.Count -gt 0) {
                if ($User.MemberOf.Count -eq 0) {
                    continue
                }
                $GroupExclude = $false
                foreach ($Group in $User.MemberOf) {
                    if ($GroupIDsExclude.Keys -contains $Group.Id) {
                        $GroupExclude = $true
                        break
                    }
                }
                if ($GroupExclude -eq $true) {
                    Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by group exclusion"
                    continue
                }
                $GroupInclude = $false
                foreach ($Group in $User.MemberOf) {
                    if ($GroupIDs.Keys -contains $Group.Id) {
                        $GroupInclude = $true
                        break
                    }
                }
                if ($GroupInclude -eq $false) {
                    Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by group inclusion"
                    continue
                }
            }
            foreach ($Property in $PropertyFilterExclude.Keys) {
                $Filter = $PropertyFilterExclude[$Property]
                $Value = $User.$Property
                if ($Filter.Operator -eq 'Like') {
                    $Find = $false
                    foreach ($FilterValue in $Filter.Value) {
                        if ($Value -like $FilterValue) {
                            $Find = $true
                        }
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'Equal') {
                    $Find = $false
                    if ($Filter.Value -contains $Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'NotEqual') {
                    $Find = $false
                    if ($Filter.Value -notcontains $Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'LessThan') {
                    $Find = $false
                    if ($Value -lt $Filter.Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'MoreThan') {
                    $Find = $false
                    if ($Value -gt $Filter.Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } else {
                    Write-Color -Text "[e] ", "Unknown operator: $($Filter.Operator)" -Color Red, White
                    return $false
                }
            }
            foreach ($Property in $PropertyFilter.Keys) {
                $Filter = $PropertyFilter[$Property]
                $Value = $User.$Property
                if ($Filter.Operator -eq 'Like') {
                    $Find = $false
                    foreach ($FilterValue in $Filter.Value) {
                        if ($Value -like $FilterValue) {
                            $Find = $true
                        }
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'Equal') {
                    $Find = $false
                    if ($Filter.Value -contains $Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'NotEqual') {
                    $Find = $false
                    if ($Filter.Value -notcontains $Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'LessThan') {
                    $Find = $false
                    if ($Value -lt $Filter.Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'MoreThan') {
                    $Find = $false
                    if ($Value -gt $Filter.Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out user $($User.UserPrincipalName) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } else {
                    Write-Color -Text "[e] ", "Unknown operator: $($Filter.Operator)" -Color Red, White
                    return $false
                }
            }
            Add-Member -MemberType NoteProperty -Name 'Type' -Value $User.UserType -InputObject $User
            $Entry = $User.Id
            $ExistingUsers[$Entry] = $User
        }
    }
    if ($MemberTypes -contains 'Contact') {
        try {
            $getMgContactSplat = @{
                Property    = $Script:PropertiesContacts
                All         = $true
                ErrorAction = 'Stop'
            }
            if ($GroupIDs.Keys.Count -gt 0) {
                $getMgContactSplat.ExpandProperty = 'memberOf'
            }
            $Users = Get-MgContact @getMgContactSplat
        } catch {
            Write-Color -Text "[e] ", "Failed to get contacts. ", "Error: $($_.Exception.Message)" -Color Red, White, Red
            return $false
        }
        :NextUser foreach ($User in $Users) {
            $Entry = $User.Id

            if ($GroupIDs.Keys.Count -gt 0) {
                if ($User.MemberOf.Count -eq 0) {
                    continue
                }
                $GroupExclude = $false
                foreach ($Group in $User.MemberOf) {
                    if ($GroupIDsExclude.Keys -contains $Group.Id) {
                        $GroupExclude = $true
                        break
                    }
                }
                if ($GroupExclude -eq $true) {
                    Write-Verbose -Message "Filtering out contact $($User.MailNickname) by group exclusion"
                    continue
                }
                $GroupInclude = $false
                foreach ($Group in $User.MemberOf) {
                    if ($GroupIDs.Keys -contains $Group.Id) {
                        $GroupInclude = $true
                        break
                    }
                }
                if ($GroupInclude -eq $false) {
                    Write-Verbose -Message "Filtering out contact $($User.MailNickname) by group inclusion"
                    continue
                }
            }
            foreach ($Property in $PropertyFilterExclude.Keys) {
                $Filter = $PropertyFilterExclude[$Property]
                $Value = $User.$Property
                if ($Filter.Operator -eq 'Like') {
                    $Find = $false
                    foreach ($FilterValue in $Filter.Value) {
                        if ($Value -like $FilterValue) {
                            $Find = $true
                        }
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'Equal') {
                    $Find = $false
                    if ($Filter.Value -contains $Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'NotEqual') {
                    $Find = $false
                    if ($Filter.Value -notcontains $Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'LessThan') {
                    $Find = $false
                    if ($Value -lt $Filter.Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'MoreThan') {
                    $Find = $false
                    if ($Value -gt $Filter.Value) {
                        $Find = $true
                    }
                    if ($Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) matching $($Filter.Value)"
                        continue NextUser
                    }
                } else {
                    Write-Color -Text "[e] ", "Unknown operator: $($Filter.Operator)" -Color Red, White
                    return $false
                }
            }

            foreach ($Property in $PropertyFilter.Keys) {
                $Filter = $PropertyFilter[$Property]
                $Value = $User.$Property
                if ($Filter.Operator -eq 'Like') {
                    $Find = $false
                    foreach ($FilterValue in $Filter.Value) {
                        if ($Value -like $FilterValue) {
                            $Find = $true
                        }
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'Equal') {
                    $Find = $false
                    if ($Filter.Value -contains $Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'NotEqual') {
                    $Find = $false
                    if ($Filter.Value -notcontains $Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'LessThan') {
                    $Find = $false
                    if ($Value -lt $Filter.Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } elseif ($Filter.Operator -eq 'MoreThan') {
                    $Find = $false
                    if ($Value -gt $Filter.Value) {
                        $Find = $true
                    }
                    if (-not $Find) {
                        Write-Verbose -Message "Filtering out contact $($User.MailNickname) by property $($Property) not matching $($Filter.Value)"
                        continue NextUser
                    }
                } else {
                    Write-Color -Text "[e] ", "Unknown operator: $($Filter.Operator)" -Color Red, White
                    return $false
                }
            }

            $NewUser = [ordered] @{
                Id             = $User.Id                           
                Type           = 'Contact'                       
                MobilePhone    = $User.MobilePhone                  
                BusinessPhones = $User.BusinessPhones               
                Country        = $User.Addresses.CountryOrRegion                      
                City           = $User.Addresses.City                                 
                State          = $User.Addresses.State                                
                Street         = $User.Addresses.Street                               
                PostalCode     = $User.Addresses.PostalCode                           
                CompanyName    = $User.CompanyName                  

                DisplayName    = $User.DisplayName                  
                GivenName      = $User.GivenName                    
                JobTitle       = $User.JobTitle                     
                Mail           = $User.Mail                         
                MailNickname   = $User.MailNickname                 

                MemberOf       = $User.MemberOf                     

                Mobile         = $User.Mobile                       

                Surname        = $User.Surname                      

            }
            foreach ($Phone in $User.Phones) {
                if ($Phone.Type -eq 'Mobile') {
                    $NewUser.MobilePhone = $Phone.Number
                } elseif ($Phone.Type -eq 'Business') {
                    $NewUser.BusinessPhones = $Phone.Number
                } elseif ($Phone.Type -eq 'Home') {
                    $NewUser.HomePhone = $Phone.Number
                }
            }

            $ExistingUsers[$Entry] = [PSCustomObject] $NewUser
        }
    }
    $ExistingUsers
}
function Get-O365ExistingUserContacts {
    [cmdletbinding()]
    param(
        [string] $UserID,
        [string] $GuidPrefix,
        [string] $FolderName
    )

    $ExistingContacts = [ordered] @{}
    if ($FolderName) {
        try {
            $CurrentContactsFolder = Get-MgUserContactFolder -UserId $UserId -Filter "DisplayName eq '$FolderName'" -ExpandProperty Contacts -ErrorAction Stop -All
        } catch {
            Write-Color -Text "[!] ", "Getting user folder ", $FolderName, " failed for ", $UserId, ". Error: ", $_.Exception.Message -Color Red, White, Red, White
            return
        }
        if (-not $CurrentContactsFolder) {
            Write-Color -Text "[!] ", "User folder ", $FolderName, " not found for ", $UserId -Color Yellow, Yellow, Red, Yellow, Red
            return
        }

        $CurrentContacts = $CurrentContactsFolder.Contacts
    } else {
        try {
            $CurrentContacts = Get-MgUserContact -UserId $UserId -All -ErrorAction Stop
        } catch {
            Write-Color -Text "[!] ", "Getting user contacts for ", $UserId, " failed. Error: ", $_.Exception.Message -Color Red, White, Red
            return
        }
    }
    foreach ($Contact in $CurrentContacts) {
        if (-not $Contact.FileAs) {
            continue
        }

        if ($GuidPrefix -and -not $Contact.FileAs.StartsWith($GuidPrefix)) {
            continue
        } elseif ($GuidPrefix -and $Contact.FileAs.StartsWith($GuidPrefix)) {
            $Contact.FileAs = $Contact.FileAs.Substring($GuidPrefix.Length)
        }

        $Guid = [guid]::Empty
        $ConversionWorked = [guid]::TryParse($Contact.FileAs, [ref]$Guid)
        if (-not $ConversionWorked) {
            continue
        }

        $Entry = [string]::Concat($Contact.FileAs)
        $ExistingContacts[$Entry] = $Contact
    }

    Write-Color -Text "[i] ", "User ", $UserId, " has ", $CurrentContacts.Count, " contacts, out of which ", $ExistingContacts.Count, " synchronized." -Color Yellow, White, Cyan, White, Cyan, White, Cyan, White
    Write-Color -Text "[i] ", "Users to process: ", $ExistingUsers.Count, " Contacts to process: ", $ExistingContacts.Count -Color Yellow, White, Cyan, White, Cyan
    $ExistingContacts
}
function Initialize-DefaultValuesO365 {
    [cmdletBinding()]
    param(

    )

    $Script:PropertiesUsers = @(
        'DisplayName'
        'GivenName'
        'Surname'
        'Mail'
        'Nickname'
        'MobilePhone'
        'HomePhone'
        'BusinessPhones'
        'UserPrincipalName'
        'Id',
        'UserType'
        'EmployeeType'
        'AccountEnabled'
        'CreatedDateTime'
        'AssignedLicenses'
        'MemberOf'
        'MobilePhone'
        'HomePhone'
        'BusinessPhones'
        'CompanyName'
        'JobTitle'
        'EmployeeId'
        'Country'
        'City'
        'State'
        'Street'
        'PostalCode'
    )

    $Script:PropertiesContacts = @(
        'DisplayName'
        'GivenName'
        'Surname'
        'Mail'
        'JobTitle'
        'MailNickname'

        'UserPrincipalName'
        'Id',
        'CompanyName'
        'OnPremisesSyncEnabled'
        'Addresses'
        'MemberOf'
        'MobilePhone'
        'Phones'
        'HomePhone'
        'BusinessPhones'
        'CompanyName'
        'JobTitle'
        'EmployeeId'
        'Country'
        'City'
        'State'
        'Street'
        'PostalCode'
    )

    $Script:MappingContactToUser = [ordered] @{
        'MailNickname'   = 'NickName'
        'DisplayName'    = 'DisplayName'
        'GivenName'      = 'GivenName'
        'Surname'        = 'Surname'

        'Mail'           = 'EmailAddresses.Address'
        'MobilePhone'    = 'MobilePhone'
        'HomePhone'      = 'HomePhone'
        'CompanyName'    = 'CompanyName'
        'BusinessPhones' = 'BusinessPhones'
        'JobTitle'       = 'JobTitle'
        'Country'        = 'BusinessAddress.CountryOrRegion'
        'City'           = 'BusinessAddress.City'
        'State'          = 'BusinessAddress.State'
        'Street'         = 'BusinessAddress.Street'
        'PostalCode'     = 'BusinessAddress.PostalCode'
    }
}
function Initialize-FolderName {
    [cmdletbinding()]
    param(
        [string] $UserId,
        [string] $FolderName
    )
    if ($FolderName) {
        $FolderInformation = Get-MgUserContactFolder -UserId $UserId -Filter "DisplayName eq '$FolderName'"
        if (-not $FolderInformation) {
            Write-Color -Text "[!] ", "User folder ", $FolderName, " not found for ", $UserId -Color Yellow, Yellow, Red, Yellow, Red

            try {
                $FolderInformation = New-MgUserContactFolder -UserId $UserId -DisplayName $FolderName -ErrorAction Stop
            } catch {
                Write-Color -Text "[!] ", "Creating user folder ", $FolderName, " failed for ", $UserId, ". Error: ", $_.Exception.Message -Color Red, White, Red, White, Red, White
                return $false
            }
            if (-not $FolderInformation) {
                Write-Color -Text "[!] ", "Creating user folder ", $FolderName, " failed for ", $UserId -Color Red, White, Red, White
                return $false
            } else {
                Write-Color -Text "[+] ", "User folder ", $FolderName, " created for ", $UserId -Color Yellow, White, Green, White
            }
        }
        $FolderInformation
    }
}
function New-O365InternalContact {
    [CmdletBinding()]
    param(
        [string] $UserId,
        [PSCustomObject] $User,
        [string] $GuidPrefix,
        [switch] $RequireEmailAddress,
        [object] $FolderInformation
    )
    if ($RequireEmailAddress) {
        if (-not $User.Mail) {

            continue
        }
    }
    if ($User.Mail) {
        Write-Color -Text "[+] ", "Creating ", $User.DisplayName, " / ", $User.Mail -Color Yellow, White, Green, White, Green
    } else {
        Write-Color -Text "[+] ", "Creating ", $User.DisplayName -Color Yellow, White, Green, White, Green
    }
    $PropertiesToUpdate = [ordered] @{}
    foreach ($Property in $Script:MappingContactToUser.Keys) {
        $PropertiesToUpdate[$Property] = $User.$Property
    }
    try {
        $newO365WrapperPersonalContactSplat = @{
            UserId      = $UserID
            WhatIf      = $WhatIfPreference
            FileAs      = "$($GuidPrefix)$($User.Id)"
            ErrorAction = 'SilentlyContinue'
        }
        if ($FolderInformation) {
            $newO365WrapperPersonalContactSplat['ContactFolderID'] = $FolderInformation.Id
        }
        $StatusNew = New-O365WrapperPersonalContact @newO365WrapperPersonalContactSplat @PropertiesToUpdate
        $ErrorMessage = ''
    } catch {
        $ErrorMessage = $_.Exception.Message
        if ($User.Mail) {
            Write-Color -Text "[!] ", "Failed to create contact for ", $User.DisplayName, " / ", $User.Mail, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
        } else {
            Write-Color -Text "[!] ", "Failed to create contact for ", $User.DisplayName, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
        }
    }
    if ($WhatIfPreference) {
        $Status = 'OK (WhatIf)'
    } elseif ($StatusNew -eq $true) {
        $Status = 'OK'
    } else {
        $Status = 'Failed'
    }
    [PSCustomObject] @{
        UserId      = $UserId
        Action      = 'New'
        Status      = $Status
        DisplayName = $User.DisplayName
        Mail        = $User.Mail
        Skip        = ''
        Update      = $newMgUserContactSplat.Keys | Sort-Object
        Details     = ''
        Error       = $ErrorMessage
    }
}
function New-O365InternalGuest {
    [CmdletBinding()]
    param(

    )
}
function New-O365InternalHTMLReport {
    [CmdletBinding()]
    param(

    )

}
function New-O365OrgContact {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Object] $Source
    )
    Write-Color -Text "[+] ", "Adding ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress -Color Yellow, White, Cyan, White, Cyan
    try {
        $Created = New-MailContact -DisplayName $Source.DisplayName -ExternalEmailAddress $Source.PrimarySmtpAddress -Name $Source.Name -WhatIf:$WhatIfPreference -ErrorAction Stop
    } catch {
        Write-Color -Text "[e] ", "Failed to create contact. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Yellow, White, Red
    }
    if ($Created) {
        $null = Set-O365OrgContact -MailContact $Created -Contact @{} -Source $Source -SourceContact $SourceContact
    }
}
function New-O365WrapperPersonalContact {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [string] $UserId,
        [string] $AssistantName,

        [DateTime] $Birthday,

        [alias('Street', 'StreetAddress')][string] $BusinessStreet,
        [alias('City')][string] $BusinessCity,
        [alias('State')][string] $BusinessState,
        [alias('PostalCode')][string] $BusinessPostalCode,
        [alias('Country')][string] $BusinessCountryOrRegion,

        [string] $HomeStreet,
        [string] $HomeCity,
        [string] $HomeState,
        [string] $HomePostalCode,
        [string] $HomeCountryOrRegion,

        [string] $OtherAddress,
        [string] $OtherCity,
        [string] $OtherState,
        [string] $OtherPostalCode,
        [string] $OtherCountryOrRegion,

        [string] $BusinessHomePage,
        [string[]] $BusinessPhones,
        [string[]] $Categories,
        [string[]] $Children,
        [string] $CompanyName,

        [string] $Department,
        [string] $DisplayName,
        [alias('Mail')][string[]] $EmailAddresses,

        [parameter(Mandatory)][string] $FileAs,
        [string] $Generation,
        [string] $GivenName,

        [string[]]$HomePhones,
        [string[]] $ImAddresses,
        [string] $Initials,
        [string] $JobTitle,
        [string] $Manager,
        [string] $MiddleName,
        [string] $MobilePhone,
        [alias('MailNickname')][string] $NickName,
        [string] $OfficeLocation,

        [string] $ContactFolderID,
        [string] $PersonalNotes,
        #$Photo,
        [string] $Profession,
        [string] $SpouseName,
        [string] $Surname,
        [string] $Title,
        [string] $YomiCompanyName,
        [string] $YomiGivenName,
        [string] $YomiSurname
    )

    $ContactSplat = [ordered] @{
        UserId           = $UserId
        AssistantName    = $AssistantName
        Birthday         = $Birthday
        BusinessAddress  = @{
            Street          = $BusinessStreet
            City            = $BusinessCity
            State           = $BusinessState
            PostalCode      = $BusinessPostalCode
            CountryOrRegion = $BusinessCountryOrRegion
        }
        BusinessHomePage = $BusinessHomePage
        BusinessPhones   = $BusinessPhones
        Categories       = $Categories
        Children         = $Children
        CompanyName      = $CompanyName
        Department       = $Department
        DisplayName      = $DisplayName
        EmailAddresses   = @(
            foreach ($Email in $EmailAddresses) {
                @{
                    Address = $Email
                }
            }
        )
        Extensions       = $Extensions
        FileAs           = $FileAs
        Generation       = $Generation
        GivenName        = $GivenName
        HomeAddress      = @{
            Street          = $HomeStreet
            City            = $HomeCity
            State           = $HomeState
            PostalCode      = $HomePostalCode
            CountryOrRegion = $HomeCountryOrRegion
        }
        HomePhones       = $HomePhones
        ImAddresses      = $ImAddresses
        Initials         = $Initials
        JobTitle         = $JobTitle
        Manager          = $Manager
        MiddleName       = $MiddleName
        MobilePhone      = $MobilePhone
        NickName         = $NickName
        OfficeLocation   = $OfficeLocation
        OtherAddress     = @{
            Street          = $OtherStreet
            City            = $OtherCity
            State           = $OtherState
            PostalCode      = $OtherPostalCode
            CountryOrRegion = $OtherCountryOrRegion
        }
        ContactFolderID  = $ContactFolderID
        PersonalNotes    = $PersonalNotes
        Profession       = $Profession
        SpouseName       = $SpouseName
        Surname          = $Surname
        Title            = $Title
        YomiCompanyName  = $YomiCompanyName
        YomiGivenName    = $YomiGivenName
        YomiSurname      = $YomiSurname
        WhatIf           = $WhatIfPreference
        ErrorAction      = 'Stop'
    }
    Remove-EmptyValue -Hashtable $ContactSplat -Recursive -Rerun 2

    if ([string]::IsNullOrEmpty($ContactFolderID)) {
        try {
            $null = New-MgUserContact @contactSplat
            $true
        } catch {
            Write-Color -Text "[!] ", "Failed to create contact for ", $DisplayName, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
            $false
        }
    } else {
        try {
            $null = New-MgUserContactFolderContact @contactSplat
            $true
        } catch {
            Write-Color -Text "[!] ", "Failed to create contact for ", $DisplayName, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
            $false
        }
    }
}
function Remove-O365InternalContact {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [System.Collections.Generic.List[object]] $ToPotentiallyRemove,
        [System.Collections.IDictionary] $ExistingUsers,
        [System.Collections.IDictionary] $ExistingContacts,
        [string] $UserId
    )
    foreach ($ContactID in $ExistingContacts.Keys) {
        $Contact = $ExistingContacts[$ContactID]
        $Entry = $Contact.FileAs
        if ($ExistingUsers[$Entry]) {

        } else {
            Write-Color -Text "[x] ", "Removing (not required) ", $Contact.DisplayName -Color Yellow, White, Red, White, Red
            try {
                Remove-MgUserContact -UserId $UserId -ContactId $Contact.Id -WhatIf:$WhatIfPreference -ErrorAction Stop
                if ($WhatIfPreference) {
                    $Status = 'OK (WhatIf)'
                } else {
                    $Status = 'OK'
                }
                $ErrorMessage = ''
            } catch {
                $Status = 'Failed'
                $ErrorMessage = $_.Exception.Message
                Write-Color -Text "[!] ", "Failed to remove contact for ", $Contact.DisplayName, " / ", $Contact.Mail, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
            }
            $OutputObject = [PSCustomObject] @{
                UserId      = $UserId
                Action      = 'Remove'
                Status      = $Status
                DisplayName = $Contact.DisplayName
                Mail        = $Contact.Mail
                Skip        = ''
                Update      = ''
                Details     = 'Not required'
                Error       = $ErrorMessage
            }
            $OutputObject
        }
    }
}
function Set-LoggingCapabilities {
    [CmdletBinding()]
    param(
        [string] $LogPath,
        [int] $LogMaximum,
        [switch] $ShowTime,
        [string] $TimeFormat
    )

    $Script:PSDefaultParameterValues = @{
        "Write-Color:LogFile"    = $LogPath
        "Write-Color:ShowTime"   = if ($PSBoundParameters.ContainsKey('ShowTime')) {
            $ShowTime.IsPresent } else {
            $null }
        "Write-Color:TimeFormat" = $TimeFormat
    }
    Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues

    if ($LogPath) {
        $FolderPath = [io.path]::GetDirectoryName($LogPath)
        if (-not (Test-Path -LiteralPath $FolderPath)) {
            $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
        }
        if ($LogMaximum -gt 0) {
            $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum
            if ($CurrentLogs) {
                Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                foreach ($Log in $CurrentLogs) {
                    try {
                        Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false
                        Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green
                    } catch {
                        Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                    }
                }
            }
        } else {
            Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan
        }
    }
}
function Set-O365InternalContact {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER UserID
    Identity of the user to synchronize contacts to. It can be UserID or UserPrincipalName.
 
    .PARAMETER User
    User/Contact object from GAL
 
    .PARAMETER FolderName
 
    .PARAMETER Contact
    Existing contact in user's personal contacts
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $UserID,
        [PSCustomObject] $User,
        [PSCustomObject] $Contact,
        [string] $FolderName
    )

    $OutputObject = Compare-UserToContact -ExistingContactGAL $User -Contact $Contact -UserID $UserID
    if ($OutputObject.Update.Count -gt 0) {
        if ($User.Mail) {
            Write-Color -Text "[i] ", "Updating ", $User.DisplayName, " / ", $User.Mail, " properties to update: ", $($OutputObject.Update -join ', '), " properties to skip: ", $($OutputObject.Skip -join ', ') -Color Yellow, White, Green, White, Green, White, Green, White, Cyan
        } else {
            Write-Color -Text "[i] ", "Updating ", $User.DisplayName, " properties to update: ", $($OutputObject.Update -join ', '), " properties to skip: ", $($OutputObject.Skip -join ', ') -Color Yellow, White, Green, White, Green, White, Green, White, Cyan
        }
    }

    if ($OutputObject.Update.Count -gt 0) {
        $PropertiesToUpdate = [ordered] @{}
        foreach ($Property in $OutputObject.Update) {
            $PropertiesToUpdate[$Property] = $User.$Property
        }
        $StatusSet = Set-O365WrapperPersonalContact -UserId $UserID -ContactId $Contact.Id @PropertiesToUpdate -WhatIf:$WhatIfPreference
        if ($WhatIfPreference) {
            $Status = 'OK (WhatIf)'
        } elseif ($StatusSet -eq $true) {
            $Status = 'OK'
        } else {
            $Status = 'Failed'
        }
    } else {
        $Status = 'Not required'
    }

    $OutputObject = [PSCustomObject] @{
        UserId      = $UserId
        Action      = 'Update'
        Status      = $Status
        DisplayName = $User.DisplayName
        Mail        = $User.Mail
        Skip        = ''
        Update      = ''
        Details     = ''
        Error       = $ErrorMessage
    }
    $OutputObject
}
function Set-O365OrgContact {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [System.Collections.IDictionary] $CurrentContactsCache,
        [Object] $MailContact,
        [Object] $Contact,
        [Object] $Source,
        [Object] $SourceContact
    )
    Write-Color -Text "[i] ", "Checking ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress, " for updates" -Color Yellow, White, Cyan, White, Cyan
    if ($Source -and $SourceContact) {
        if (-not $MailContact) {
            $MailContact = $CurrentContactsCache[$Source.PrimarySmtpAddress].MailContact
        }
        $MismatchedMailContact = [ordered] @{}
        [Array] $MismatchedPropertiesMailContact = foreach ($Property in $Source.PSObject.Properties.Name) {
            if ($Source.$Property -ne $MailContact.$Property) {
                if ([string]::IsNullOrEmpty($Source.$Property) -and [string]::IsNullOrEmpty($MailContact.$Property) ) {

                } else {

                    $Property
                    $MismatchedMailContact[$Property] = $Source.$Property
                }
            }
        }

        if (-not $Contact) {
            $Contact = $CurrentContactsCache[$Source.PrimarySmtpAddress].Contact
        }

        $MismatchedContact = [ordered] @{}
        [Array] $MismatchedPropertiesContact = foreach ($Property in $SourceContact.PSObject.Properties.Name) {
            if ($SourceContact.$Property -ne $Contact.$Property) {
                if ([string]::IsNullOrEmpty($SourceContact.$Property) -and [string]::IsNullOrEmpty($Contact.$Property) ) {

                } else {

                    $Property
                    $MismatchedContact[$Property] = $SourceContact.$Property
                }
            }
        }
        if ($MismatchedPropertiesMailContact.Count -gt 0 -or $MismatchedPropertiesContact.Count -gt 0) {
            Write-Color -Text "[i] ", "Mismatched properties for ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress, " are: ", ($MismatchedPropertiesMailContact + $MismatchedPropertiesContact -join ', ') -Color Yellow, White, DarkCyan, White, Cyan
            $ErrorValue = $false
            if ($MismatchedPropertiesMailContact.Count -gt 0) {
                Write-Color -Text "[*] ", "Updating mail contact for ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress -Color Yellow, Green, DarkCyan, White, Cyan
                try {
                    Set-MailContact -Identity $MailContact.Identity -WhatIf:$WhatIfPreference -ErrorAction Stop @MismatchedMailContact
                } catch {
                    $ErrorValue = $true
                    Write-Color -Text "[e] ", "Failed to update mail contact. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Red, White, Red
                }
            }
            if ($MismatchedPropertiesContact.Count -gt 0) {
                Write-Color -Text "[*] ", "Updating contact for ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress -Color Yellow, Green, DarkCyan, White, Cyan
                try {
                    Set-Contact -Identity $MailContact.Identity -WhatIf:$WhatIfPreference -ErrorAction Stop @MismatchedContact
                } catch {
                    $ErrorValue = $true
                    Write-Color -Text "[e] ", "Failed to update contact. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Red, White, Red
                }
            }
            if ($ErrorValue -eq $false) {
                $true
            }
        } else {

        }
    }
}
function Set-O365WrapperPersonalContact {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [string] $ContactId,
        [string] $UserId,
        [string] $AssistantName,

        [DateTime] $Birthday,

        [alias('Street', 'StreetAddress')][string] $BusinessStreet,
        [alias('City')][string] $BusinessCity,
        [alias('State')][string] $BusinessState,
        [alias('PostalCode')][string] $BusinessPostalCode,
        [alias('Country')][string] $BusinessCountryOrRegion,

        [string] $HomeStreet,
        [string] $HomeCity,
        [string] $HomeState,
        [string] $HomePostalCode,
        [string] $HomeCountryOrRegion,

        [string] $OtherAddress,
        [string] $OtherCity,
        [string] $OtherState,
        [string] $OtherPostalCode,
        [string] $OtherCountryOrRegion,

        [string] $BusinessHomePage,
        [string[]] $BusinessPhones,
        [string[]] $Categories,
        [string[]] $Children,
        [string] $CompanyName,

        [string] $Department,
        [string] $DisplayName,
        [alias('Mail')][string[]] $EmailAddresses,

        [string] $FileAs,
        [string] $Generation,
        [string] $GivenName,

        [string[]]$HomePhones,
        [string[]] $ImAddresses,
        [string] $Initials,
        [string] $JobTitle,
        [string] $Manager,
        [string] $MiddleName,
        [string] $MobilePhone,
        [string] $NickName,
        [string] $OfficeLocation,

        [string] $ParentFolderId,
        [string] $PersonalNotes,
        #$Photo,
        [string] $Profession,
        [string] $SpouseName,
        [string] $Surname,
        [string] $Title,
        [string] $YomiCompanyName,
        [string] $YomiGivenName,
        [string] $YomiSurname
    )

    $ContactSplat = [ordered] @{
        ContactId        = $ContactId
        UserId           = $UserId
        AssistantName    = $AssistantName
        Birthday         = $Birthday
        BusinessAddress  = @{
            Street          = $BusinessStreet
            City            = $BusinessCity
            State           = $BusinessState
            PostalCode      = $BusinessPostalCode
            CountryOrRegion = $BusinessCountryOrRegion
        }
        BusinessHomePage = $BusinessHomePage
        BusinessPhones   = $BusinessPhones
        Categories       = $Categories
        Children         = $Children
        CompanyName      = $CompanyName
        Department       = $Department
        DisplayName      = $DisplayName
        EmailAddresses   = @(
            foreach ($Email in $EmailAddresses) {
                @{
                    Address = $Email
                }
            }
        )
        Extensions       = $Extensions
        FileAs           = $FileAs
        Generation       = $Generation
        GivenName        = $GivenName
        HomeAddress      = @{
            Street          = $HomeStreet
            City            = $HomeCity
            State           = $HomeState
            PostalCode      = $HomePostalCode
            CountryOrRegion = $HomeCountryOrRegion
        }
        HomePhones       = $HomePhones
        ImAddresses      = $ImAddresses
        Initials         = $Initials
        JobTitle         = $JobTitle
        Manager          = $Manager
        MiddleName       = $MiddleName
        MobilePhone      = $MobilePhone
        NickName         = $NickName
        OfficeLocation   = $OfficeLocation
        OtherAddress     = @{
            Street          = $OtherStreet
            City            = $OtherCity
            State           = $OtherState
            PostalCode      = $OtherPostalCode
            CountryOrRegion = $OtherCountryOrRegion
        }
        ParentFolderId   = $ParentFolderId
        PersonalNotes    = $PersonalNotes
        Profession       = $Profession
        SpouseName       = $SpouseName
        Surname          = $Surname
        Title            = $Title
        YomiCompanyName  = $YomiCompanyName
        YomiGivenName    = $YomiGivenName
        YomiSurname      = $YomiSurname
        WhatIf           = $WhatIfPreference
        ErrorAction      = 'Stop'
    }
    Remove-EmptyValue -Hashtable $ContactSplat -Recursive -Rerun 2

    try {
        $null = Update-MgUserContact @contactSplat
        $true
    } catch {
        $false
        Write-Color -Text "[!] ", "Failed to update contact for ", $ContactSplat.DisplayName, " / ", $ContactSplat.EmailAddresses, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
    }
}
function Sync-InternalO365PersonalContact {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER UserId
    Parameter description
 
    .PARAMETER MemberTypes
    Parameter description
 
    .PARAMETER RequireEmailAddress
    Parameter description
 
    .PARAMETER GuidPrefix
    Parameter description
 
    .PARAMETER FolderInformation
    Parameter description
 
    .PARAMETER ExistingUsers
    Users and contacts in GAL that will be synchronized to user's personal contacts
 
    .PARAMETER ExistingContacts
    Existing contacts in user's personal contacts
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [string] $UserId,
        [ValidateSet('Member', 'Guest', 'Contact')][string[]] $MemberTypes,
        [switch] $RequireEmailAddress,
        [string] $GuidPrefix,
        [object] $FolderInformation,
        [System.Collections.IDictionary] $ExistingUsers,
        [System.Collections.IDictionary] $ExistingContacts
    )
    $ListActions = [System.Collections.Generic.List[object]]::new()
    foreach ($UsersInternalID in $ExistingUsers.Keys) {
        $User = $ExistingUsers[$UsersInternalID]
        if ($User.Mail) {
            Write-Color -Text "[i] ", "Processing ", $User.DisplayName, " / ", $User.Mail -Color Yellow, White, Cyan, White, Cyan
        } else {
            Write-Color -Text "[i] ", "Processing ", $User.DisplayName -Color Yellow, White, Cyan
        }
        $Entry = $User.Id
        $Contact = $ExistingContacts[$Entry]

        if ($Contact) {

            $OutputObject = Set-O365InternalContact -UserID $UserId -User $User -Contact $Contact
            $ListActions.Add($OutputObject)
        } else {

            $OutputObject = New-O365InternalContact -UserId $UserId -User $User -GuidPrefix $GuidPrefix -RequireEmailAddress:$RequireEmailAddress -FolderInformation $FolderInformation
            $ListActions.Add($OutputObject)
        }
    }

    $RemoveActions = Remove-O365InternalContact -ExistingUsers $ExistingUsers -ExistingContacts $ExistingContacts -UserId $UserId
    foreach ($Remove in $RemoveActions) {
        $ListActions.Add($Remove)
    }
    $ListActions
}
function Clear-O365PersonalContact {
    <#
    .SYNOPSIS
    Removes personal contacts from user on Office 365.
 
    .DESCRIPTION
    Removes personal contacts from user on Office 365.
    By default it will only remove contacts that were synchronized by O365Synchronizer.
    If you want to remove all contacts use -All parameter.
 
    .PARAMETER Identity
    Identity of the user to remove contacts from.
 
    .PARAMETER GuidPrefix
    Prefix of the GUID that is used to identify contacts that were synchronized by O365Synchronizer.
    By default no prefix is used, meaning GUID of the user will be used as File, As property of the contact.
 
    .PARAMETER FolderName
    Name of the folder to remove contacts from. If not set it will remove contacts from the main folder.
 
    .PARAMETER FolderRemove
    If set it will remove the folder as well, once the contacts are removed.
 
    .PARAMETER FullLogging
    If set it will log all actions. By default it will only log actions that meant contact is getting removed or an error happens.
 
    .PARAMETER All
    If set it will remove all contacts. By default it will only remove contacts that were synchronized by O365Synchronizer.
 
    .EXAMPLE
    Clear-O365PersonalContact -Identity 'przemyslaw.klys@test.pl' -WhatIf
 
    .EXAMPLE
    Clear-O365PersonalContact -Identity 'przemyslaw.klys@test.pl' -GuidPrefix 'O365' -WhatIf
 
    .EXAMPLE
    Clear-O365PersonalContact -Identity 'przemyslaw.klys@test.pl' -All -WhatIf
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string] $Identity,
        [string] $GuidPrefix,
        [string] $FolderName,
        [switch] $FolderRemove,
        [switch] $FullLogging,
        [switch] $All
    )
    if ($FolderName) {
        try {
            $CurrentContactsFolder = Get-MgUserContactFolder -UserId $Identity -Filter "DisplayName eq '$FolderName'" -ExpandProperty Contacts -ErrorAction Stop -All
        } catch {
            Write-Color -Text "[!] ", "Getting user folder ", $FolderName, " failed for ", $Identity, ". Error: ", $_.Exception.Message -Color Red, White, Red, White
            return
        }
        if (-not $CurrentContactsFolder) {
            Write-Color -Text "[!] ", "User folder ", $FolderName, " not found for ", $Identity -Color Yellow, Yellow, Red, Yellow, Red
            return
        }

        $CurrentContacts = $CurrentContactsFolder.Contacts
    } else {
        try {
            $CurrentContacts = Get-MgUserContact -UserId $Identity -All -ErrorAction Stop
        } catch {
            Write-Color -Text "[!] ", "Getting user contacts for ", $Identity, " failed. Error: ", $_.Exception.Message -Color Red, White, Red
            return
        }
    }
    foreach ($Contact in $CurrentContacts) {
        if ($GuidPrefix -and -not $Contact.FileAs.StartsWith($GuidPrefix)) {
            if (-not $All) {
                if ($FullLogging) {
                    Write-Color -Text "[i] ", "Skipping ", $Contact.Id, " because it is not created as part of O365Synchronizer." -Color Yellow, White, DarkYellow, White
                }
                continue
            }
        } elseif ($GuidPrefix -and $Contact.FileAs.StartsWith($GuidPrefix)) {
            $Contact.FileAs = $Contact.FileAs.Substring($GuidPrefix.Length)
        }
        $Guid = [guid]::Empty
        $ConversionWorked = [guid]::TryParse($Contact.FileAs, [ref]$Guid)
        if (-not $ConversionWorked) {
            if (-not $All) {
                if ($FullLogging) {
                    Write-Color -Text "[i] ", "Skipping ", $Contact.Id, " because it is not created as part of O365Synchronizer." -Color Yellow, White, DarkYellow, White
                }
                continue
            }
        }
        Write-Color -Text "[i] ", "Removing ", $Contact.DisplayName, " from ", $Identity, " (WhatIf: $WhatIfPreference)" -Color Yellow, White, Cyan, White, Cyan
        Remove-MgUserContact -UserId $Identity -ContactId $Contact.Id -WhatIf:$WhatIfPreference
    }
    if ($CurrentContactsFolder -and $FolderName -and $FolderRemove) {
        Write-Color -Text "[i] ", "Removing folder ", $FolderName, " from ", $Identity, " (WhatIf: $WhatIfPreference)" -Color Yellow, White, Cyan, White, Cyan
        try {
            Remove-MgUserContactFolder -UserId $Identity -ContactFolderId $CurrentContactsFolder.Id -WhatIf:$WhatIfPreference -ErrorAction Stop
        } catch {
            Write-Color -Text "[!] ", "Failed to remove folder ", $FolderName, " from ", $Identity, " because: ", $_.Exception.Message -Color Yellow, White, Red, White, Red, White, Red
        }
    }
}
function Sync-O365Contact {
    <#
    .SYNOPSIS
    Synchronize contacts between source and target Office 365 tenant.
 
    .DESCRIPTION
    Synchronize contacts between source and target Office 365 tenant.
    Get users from source tenant using Get-MgUser (Microsoft Graph) and provide them as source objects.
    You can specify domains to synchronize. If you don't specify domains, it will use all domains from source objects.
    During synchronization new contacts will be created matching given domains in target tenant on Exchange Online.
    If contact already exists, it will be updated if needed, even if it wasn't synchronized by this module.
    It will asses whether it needs to add/update/remove contacts based on provided domain names from source objects.
 
    .PARAMETER SourceObjects
    Source objects to synchronize. You can use Get-MgUser to get users from Microsoft Graph and provide them as source objects.
    Any filtering you apply to them is valid and doesn't have to be 1:1 conversion.
 
    .PARAMETER Domains
    Domains to synchronize. If not specified, it will use all domains from source objects.
 
    .PARAMETER SkipAdd
    Disable the adding of new contacts functionality. This is useful if you want to only update existing contacts or remove non-existing contacts.
 
    .PARAMETER SkipUpdate
    Disable the updating of existing contacts functionality. This is useful if you want to only add new contacts or remove non-existing contacts.
 
    .PARAMETER SkipRemove
    Disable the removing of non-existing contacts functionality. This is useful if you want to only add new contacts or update existing contacts.
 
    .EXAMPLE
    # Source tenant
    $ClientID = '9e1b3c36'
    $TenantID = 'ceb371f6'
    $ClientSecret = 'NDE8Q'
 
    $Credentials = [pscredential]::new($ClientID, (ConvertTo-SecureString $ClientSecret -AsPlainText -Force))
    Connect-MgGraph -ClientSecretCredential $Credentials -TenantId $TenantID -NoWelcome
 
    $UsersToSync = Get-MgUser | Select-Object -First 5
 
    # Destination tenant
    $ClientID = 'edc4302e'
    Connect-ExchangeOnline -AppId $ClientID -CertificateThumbprint '2EC710' -Organization 'xxxxx.onmicrosoft.com'
    Sync-O365Contact -SourceObjects $UsersToSync -Domains 'evotec.pl', 'gmail.com' -Verbose -WhatIf
 
    .NOTES
    General notes
    #>

    [cmdletbinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][Array] $SourceObjects,
        [Parameter()][Array] $Domains,
        [switch] $SkipAdd,
        [switch] $SkipUpdate,
        [switch] $SkipRemove,
        [string] $LogPath,
        [int] $LogMaximum
    )

    Write-Color -Text "[i] ", "Starting synchronization of ", $SourceObjects.Count, " objects" -Color Yellow, White, Cyan, White, Cyan

    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum

    $StartTimeLog = Start-TimeLog

    Write-Color -Text "[i] ", "Starting synchronization of ", $SourceObjects.Count, " objects" -Color Yellow, White, Cyan, White, Cyan -NoConsoleOutput

    $SourceObjectsCache = [ordered]@{}

    if (-not $Domains) {
        Write-Color -Text "[i] ", "No domains specified, will use all domains from given user base" -Color Yellow, White, Cyan
        $DomainsCache = [ordered]@{}
        [Array] $Domains = foreach ($Source in $SourceObjects) {
            if ($Source.Mail) {
                $Domain = $Source.Mail.Split('@')[1]
                if ($Domain -and -not $DomainsCache[$Domain]) {
                    $Domain
                    $DomainsCache[$Domain] = $true
                    Write-Color -Text "[i] ", "Adding ", $Domain, " to list of domains to synchronize" -Color Yellow, White, Cyan
                }
            }
        }
    }

    [Array] $ConvertedObjects = foreach ($Source in $SourceObjects) {
        Convert-GraphObjectToContact -SourceObject $Source
    }

    $CurrentContactsCache = Get-O365ContactsFromTenant -Domains $Domains
    if ($null -eq $CurrentContactsCache) {
        return
    }

    $CountAdd = 0
    $CountRemove = 0
    $CountUpdate = 0

    foreach ($Object in $ConvertedObjects) {
        $Source = $Object.MailContact
        $SourceContact = $Object.Contact
        if ($Source.PrimarySmtpAddress) {

            $Skip = $true
            foreach ($Domain in $Domains) {
                if ($Source.PrimarySmtpAddress -like "*@$Domain") {
                    $Skip = $false
                    break
                }
            }
            if ($Skip) {
                Write-Color -Text "[s] ", "Skipping ", $Source.DisplayName, " / ", $Source.PrimarySmtpAddress, " as it's not in domains to synchronize ", $($Domains -join ', ') -Color Yellow, White, Red, White, Red
                continue
            }

            $SourceObjectsCache[$Source.PrimarySmtpAddress] = $Source

            if ($CurrentContactsCache[$Source.PrimarySmtpAddress]) {

                if (-not $SkipUpdate) {
                    $Updated = Set-O365OrgContact -CurrentContactsCache $CurrentContactsCache -Source $Source -SourceContact $SourceContact
                    if ($Updated) {
                        $CountUpdate++
                    }
                }
            } else {

                if (-not $SkipAdd) {
                    New-O365OrgContact -Source $Source
                    $CountAdd++
                }
            }
        } else {

        }
    }
    if (-not $SkipRemove) {
        foreach ($C in $CurrentContactsCache.Keys) {
            $Contact = $CurrentContactsCache[$C].MailContact
            if ($SourceObjectsCache[$Contact.PrimarySmtpAddress]) {
                continue
            } else {
                Write-Color -Text "[-] ", "Removing ", $Contact.DisplayName, " / ", $Contact.PrimarySmtpAddress -Color Yellow, Red, DarkCyan, White, Cyan
                try {
                    Remove-MailContact -Identity $Contact.PrimarySmtpAddress -WhatIf:$WhatIfPreference -Confirm:$false -ErrorAction Stop
                    $CountRemove++
                } catch {
                    Write-Color -Text "[e] ", "Failed to remove contact. Error: ", ($_.Exception.Message -replace ([Environment]::NewLine), " " )-Color Yellow, White, Red
                }

            }
        }
    }
    Write-Color -Text "[i] ", "Synchronization summary: ", $CountAdd, " added, ", $CountUpdate, " updated, ", $CountRemove, " removed" -Color Yellow, White, Cyan, White, Cyan, White, Cyan, White, Cyan
    $EndTimeLog = Stop-TimeLog -Time $StartTimeLog
    Write-Color -Text "[i] ", "Finished synchronization of ", $SourceObjects.Count, " objects. ", "Time: ", $EndTimeLog -Color Yellow, White, Cyan, White, Yellow, Cyan
}
function Sync-O365PersonalContact {
    <#
    .SYNOPSIS
    Synchronizes Users, Contacts and Guests to Personal Contacts of given user.
 
    .DESCRIPTION
    Synchronizes Users, Contacts and Guests to Personal Contacts of given user.
 
    .PARAMETER Filter
    Filters to apply to users. It can be used to filter out users that you don't want to synchronize.
    You should use Sync-O365PersonalContactFilter or/and Sync-O365PersonalContactFilterGroup to create filter(s).
 
    .PARAMETER UserId
    Identity of the user to synchronize contacts to. It can be UserID or UserPrincipalName.
 
    .PARAMETER MemberTypes
    Member types to synchronize. By default it will synchronize only 'Member'. You can also specify 'Guest' and 'Contact'.
 
    .PARAMETER RequireEmailAddress
    Sync only users that have email address.
 
    .PARAMETER DoNotRequireAccountEnabled
    Do not require account to be enabled. By default account must be enabled, otherwise it will be skipped.
 
    .PARAMETER DoNotRequireAssignedLicenses
    Do not require assigned licenses. By default user must have assigned licenses, otherwise it will be skipped.
    The licenses are checked by looking at AssignedLicenses property of the user, and not the actual license types.
 
    .PARAMETER GuidPrefix
    Prefix of the GUID that is used to identify contacts that were synchronized by O365Synchronizer.
    By default no prefix is used, meaning GUID of the user will be used as File, As property of the contact.
 
    .PARAMETER FolderName
    Name of the folder to synchronize contacts to. If not set it will synchronize contacts to the main folder.
 
    .EXAMPLE
    Sync-O365PersonalContact -UserId 'przemyslaw.klys@test.pl' -Verbose -MemberTypes 'Contact', 'Member' -WhatIf
 
    .EXAMPLE
    Sync-O365PersonalContact -UserId 'przemyslaw.klys@evotec.pl' -MemberTypes 'Contact', 'Member' -GuidPrefix 'O365Synchronizer' -PassThru {
        Sync-O365PersonalContactFilter -Type Include -Property 'CompanyName' -Value 'Evotec*','Ziomek*' -Operator 'like'
        Sync-O365PersonalContactFilterGroup -Type Include -GroupID 'e7772951-4b0e-4f10-8f38-eae9b8f55962'
    } -FolderName 'O365Sync' | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [scriptblock] $Filter,
        [string[]] $UserId,
        [ValidateSet('Member', 'Guest', 'Contact')][string[]] $MemberTypes = @('Member'),
        [switch] $RequireEmailAddress,
        [string] $GuidPrefix,
        [string] $FolderName,
        [switch] $DoNotRequireAccountEnabled,
        [switch] $DoNotRequireAssignedLicenses,
        [switch] $PassThru
    )

    Initialize-DefaultValuesO365

    $getO365ExistingMembersSplat = @{
        MemberTypes             = $MemberTypes
        RequireAccountEnabled   = -not $DoNotRequireAccountEnabled.IsPresent
        RequireAssignedLicenses = -not $DoNotRequireAssignedLicenses.IsPresent
        UserProvidedFilter      = $Filter
    }

    $ExistingUsers = Get-O365ExistingMembers @getO365ExistingMembersSplat
    if ($ExistingUsers -eq $false -or $ExistingUsers -is [Array]) {
        return
    }

    foreach ($User in $UserId) {
        $FolderInformation = Initialize-FolderName -UserId $User -FolderName $FolderName
        if ($FolderInformation -eq $false) {
            return
        }

        $ExistingContacts = Get-O365ExistingUserContacts -UserID $User -GuidPrefix $GuidPrefix -FolderName $FolderName

        $Actions = Sync-InternalO365PersonalContact -FolderInformation $FolderInformation -UserId $User -ExistingUsers $ExistingUsers -ExistingContacts $ExistingContacts -MemberTypes $MemberTypes -RequireEmailAddress:$RequireEmailAddress.IsPresent -GuidPrefix $GuidPrefix -WhatIf:$WhatIfPreference
        if ($PassThru) {
            $Actions
        }
    }
}
function Sync-O365PersonalContactFilter {
    <#
    .SYNOPSIS
    Provides a way to filter out users/contacts based on properties.
 
    .DESCRIPTION
    Provides a way to filter out users/contacts based on properties.
    Only users/contacts that match the filter will be included/excluded.
 
    .PARAMETER Type
    Type of the filter. It can be 'Include' or 'Exclude'.
 
    .PARAMETER Operator
    Operator to use. It can be 'Equal', 'NotEqual', 'LessThan', 'MoreThan', 'Like'.
 
    .PARAMETER Property
    Property to use for comparison. Keep in mind that it has to exists on the object.
 
    .PARAMETER Value
    Value to compare against. It can be single value or multiple values.
 
    .EXAMPLE
    Sync-O365PersonalContact -UserId 'przemyslaw.klys@test.pl' -Verbose -MemberTypes 'Contact', 'Member' -GuidPrefix 'O365Synchronizer' -WhatIf -PassThru {
        Sync-O365PersonalContactFilter -Type Include -Property 'CompanyName' -Value 'OtherCompany*','Evotec*' -Operator 'like' # filter out on CompanyName
        Sync-O365PersonalContactFilterGroup -Type Include -GroupID 'e7772951-4b0e-4f10-8f38-eae9b8f55962' # filter out on GroupID
    } | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [ValidateSet('Include', 'Exclude')][Parameter(Mandatory)][string] $Type,
        [ValidateSet('Equal', 'NotEqual', 'LessThan', 'MoreThan', 'Like')][Parameter(Mandatory)][string] $Operator,
        [Parameter(Mandatory)][string] $Property,
        [Parameter()][Object] $Value
    )

    $Filter = [ordered] @{
        FilterType = 'Property'
        Type       = $Type
        Operator   = $Operator
        Property   = $Property
        Value      = $Value
    }
    $Filter
}
function Sync-O365PersonalContactFilterGroup {
    <#
    .SYNOPSIS
    Provides a way to filter out users/contacts based on groups.
 
    .DESCRIPTION
    Provides a way to filter out users/contacts based on groups.
    Only users/contacts that are part of the group will be included/excluded.
 
    .PARAMETER Type
    Type of the filter. It can be 'Include' or 'Exclude'.
 
    .PARAMETER GroupID
    One or multiple GroupID's to filter out users/contacts. Keep in mind that it's not the name of the group, but the actual ID of the group.
    Due to performance reasons it's better to use GroupID instead of GroupName.
 
    .EXAMPLE
    Sync-O365PersonalContact -UserId 'przemyslaw.klys@test.pl' -Verbose -MemberTypes 'Contact', 'Member' -GuidPrefix 'O365Synchronizer' -WhatIf -PassThru {
        Sync-O365PersonalContactFilter -Type Include -Property 'CompanyName' -Value 'OtherCompany*','Evotec*' -Operator 'like' # filter out on CompanyName
        Sync-O365PersonalContactFilterGroup -Type Include -GroupID 'e7772951-4b0e-4f10-8f38-eae9b8f55962' # filter out on GroupID
    } | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [ValidateSet('Include', 'Exclude')][Parameter(Mandatory)][string] $Type,
        [Parameter(Mandatory)][string[]] $GroupID
    )
    $Filter = [ordered] @{
        FilterType = 'Group'
        Type       = $Type
        GroupID    = $GroupID
    }
    $Filter
}

Export-ModuleMember -Function @('Clear-O365PersonalContact', 'Sync-O365Contact', 'Sync-O365PersonalContact', 'Sync-O365PersonalContactFilter', 'Sync-O365PersonalContactFilterGroup') -Alias @()
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBvUXXxaSj7s75m
# YfNyMex6tWodr4WW8KESmEZJy1wppaCCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCCvjHrU3iGzvpnYHF0I0vinMjyUy69+NQL3GwrvE3zKrzAN
# BgkqhkiG9w0BAQEFAASCAgCFTtgod/3gbK1MCtQWOVPB880oTmvEXZSSm5GsP8zq
# EYj+m6Vqm4T0d7H2ijvp7oV/9+HVpJ2jhkTB1mJGCg11cO+zGW+7Jhuufua86+hb
# na3FmEmo485ROVA6/8lxo2OikqKiQ1WF8JtDghdOqAcLcCBZqK+0YTy/9Qy7sOjf
# x6qZ8lpxDiu36kiOJmKUnnlhakLD8lfVKe8AapEwbcegNU+uVeDElVRSy+hJh5ZP
# UW6znJaZ1fkcpBjCdNnsTUXXsPMaC4aTB+UeMcLc/vxa1RLV7zGhiDxUXN1n4DBH
# T/mhDEUxvjqvZbLX9Xgtv8T1fiaci+NnBgQL9JykGl677I346PnLNjTSXR9gN0mj
# rXCfp5xx/ic3O4o6oviKWfQfsg44GgMFKODokF8ReRAp6QKAV0yt2pjUyAVoG5/J
# YxaNHFJSKT4PjEkZzblIUfFcSq7DP7eVV1kwZfFr9CkQN3sSkrAduqSxbYgHP3Us
# imQLATnF54Gw8CBLO45yKvO89vGLgWixcijiSP/WmS6RlrEHkK7G1Bb0R3jTzyxM
# wNbGIMg7u64RfFaWJIM5HbEDHrlcSb9IviJY0+qR3ShTemGYB7QGbCFYWkisJI3g
# L+B24yOV/EUzotga3PsX+tR21YCicx9yLKxdgjLPRweINjFZXTO04pgwf5hnM0mg
# tqGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA5MjgwNTU2MjdaMC8GCSqGSIb3DQEJBDEi
# BCCvqsmQ689waoUmeKMJO8F0npt26AVbbjceT72B1Px9DzANBgkqhkiG9w0BAQEF
# AASCAgBd1Fz9lc7u91v83wfMLwtyhqD0tPuA0d3sePur7BsLkuBGnWOkhsN3/+Yj
# ZWts99cDrIX4CDsHdq1wpkiBkYDtanHlVzO31FFOY56+Ci07px2sov6uW556iDeV
# X9CwNuKFcJxZLdXlrrFzr9z7+lRcrxcC/ksMZ68Pcnmwjcoog3pC9KqXuBnE4j/z
# Rx9Psj9Ql9hkLfXcpXfOnAxDUXxu2PRg+lQmnScmSpnxdgk0TjIWs2EQ1Lurs9ry
# cbuyowpVK6PGl6MIHtiZyrMiXe1RIFH+UyfL00N+MC+MnfC+k5Iqo4CI/nCSbj/i
# zeuD9gpDIjiUaieybsvqCWgSHCeYPvmLrBesaezq+t98OYvQMJ+hC4cyBg0O2Cpb
# ymKOFWUCVcgcdwS2GrRQZ3edLxK+HM5RULSnJ+eWRRxMm808PavPNM7tENEdxKH2
# JWaVuxNLt0HEYmupoKe+Pme2MOXyKxKz9tEDbnUTau+i0Vn9yGYce1SmyhKADXZq
# 0Odu8nF6qP9mOPIo30sYa6wyw2I2DNcJ86zcS2L4+osfbkVWv56WFMD34PNIX9lK
# VR6aq/CCHSu64ZWG9NxblWjEqlGhnY1Fm1bJCQAN/4rDI10iZdLsEQG76yMDeC1q
# dhoOGjHNolLQ21gHTWgG5GOy8fcYucCJkKa/EQKQCHWiLRKveg==
# SIG # End signature block