T2Tscripts.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\T2Tscripts.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName T2Tscripts.Import.DoDotSource -Fallback $false
if ($T2Tscripts_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName T2Tscripts.Import.IndividualFiles -Fallback $false
if ($T2Tscripts_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'T2Tscripts' -Language 'en-US'

function Assert-ServiceConnection {
    <#
    .SYNOPSIS
    Checks current connection status for SCC, EXO and AzureAD
     
    .DESCRIPTION
    Checks current connection status for SCC, EXO and AzureAD
     
    .PARAMETER Services
    List of the desired services to assert the connection to. Current available services: EXO, SCC, MicrosoftTeams, MSOnline, AzureAD, AzureADPreview, Azure, ExchangeLocal, ExchangeRemote, AD.
 
    .EXAMPLE
    PS C:\> Assert-ServiceConnection -Services ExchangeLocal, AD
    Checks current connection status for Exchange Onprem in local machine and Active Directory.
    #>

    [CmdletBinding()]
    param (
        [ValidateSet('EXO', 'SCC', 'MicrosoftTeams', 'MSOnline', 'AzureAD', 'AzureADPreview', 'Azure', 'ExchangeLocal', 'ExchangeRemote', 'AD')]
        [String[]]
        $Services
    )
    $Sessions = Get-PSSession
    $ServicesToConnect = New-Object -TypeName "System.Collections.ArrayList"

    Switch ( $Services ) {
        Azure {}
        AzureAD {
            try {
            $Null = Get-AzureADCurrentSessionInfo -ErrorAction Stop
            }
            catch {
                $null = $ServicesToConnect.add("AzureAD")
            }
        }
        AzureADPreview {}
        MSOnline {}
        MicrosoftTeams {}
        SCC {
            if ( -not ($Sessions.ComputerName -match "ps.compliance.protection.outlook.com") ) { $null = $ServicesToConnect.add("SCC") }
        }
        EXO {
            if ( $Sessions.ComputerName -notcontains "outlook.office365.com" ) { $null = $ServicesToConnect.add("EXO") }
        }
        ExchangeLocal {
            if ( $sessions.name -notmatch "Session" ) { $null = $ServicesToConnect.add("ExchangeLocal") }
        }
        ExchangeRemote {
            if ( $sessions.name -notmatch "WinRM" ) { $null = $ServicesToConnect.add("ExchangeRemote") }
        }
        AD {
            if ( -not (Get-Module ActiveDirectory -ListAvailable) ) {
            
                $null = $ServicesToConnect.add("AD")
            
            } else {

                    Import-Module ActiveDirectory
                    # Variable to be used when the machine is not
                    # an Exchange but the AD module is installed
                    [switch]$LocalAD = $True

            }
        }
    }
    return $ServicesToConnect
    return $LocalAD
}

function Connect-OnlineServices {
    <#
    .SYNOPSIS
    Connect to Online Services.
 
    .DESCRIPTION
    Use this function to connect to EXO, Exchange Onprem and Active Directory.
 
    .PARAMETER AdminUPN
    Passes the administrator's UPN to be used in the authentication prompts.
 
    .PARAMETER Services
    List of the desired services to connect to. Current available services: EXO, ExchangeLocal, ExchangeRemote, AD.
 
    .PARAMETER ExchangeHostname
    Used to inform the Exchange Server FQDN that the script will connect.
 
    .PARAMETER Confirm
    If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER WhatIf
    If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
    
    .EXAMPLE
    PS C:\> Connect-OnlineServices -EXO
    Connects to Exchange Online.
     
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Low')]
    param(
        [String]
        $AdminUPN,

        [ValidateSet('EXO','ExchangeLocal', 'ExchangeRemote', 'AD')]
        [String[]]
        $Services,
     
        [Parameter(Mandatory = $false, HelpMessage="Enter the remote exchange hostname")]
        [string]$ExchangeHostname
     )

    Switch ( $Services ) {

        EXO {
            Invoke-PSFProtectedCommand -Action "Connecting to Exchange Online" -Target "EXO" -ScriptBlock {
                Write-PSFMessage -Level Output -Message "Connecting to Exchange Online"
                try {

                    Connect-ExchangeOnline -UserPrincipalName $AdminUPN -ShowProgress:$True -ShowBanner:$False -Prefix EX
                    Write-PSFMessage -Level Output -Message "Connected to Exchange Online"

                }
                catch {
                    return $_
                }
            } -EnableException $true -PSCmdlet $PSCmdlet
        }

        ExchangeLocal {
            Invoke-PSFProtectedCommand -Action "Connecting to Exchange Onprem locally." -Target "ExchangeLocal" -ScriptBlock {
                Write-PSFMessage -Level Output -Message "Connecting to Exchange Onprem locally."
                try {
                    Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn
                    Write-PSFMessage -Level Output -Message "Connected to Exchange Onprem locally."
                }
                catch {}
            } -EnableException $true -PSCmdlet $PSCmdlet
        }

        ExchangeRemote {
            Invoke-PSFProtectedCommand -Action "Connecting to Exchange Onprem remotely." -Target "ExchangeRemote" -ScriptBlock {
                Write-PSFMessage -Level Output -Message "Connecting to Exchange Onprem remotely."
                try {
                    $exOnPremSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$ExchangeHostname/PowerShell/ -Authentication Kerberos
                    Import-PSSession $exOnPremSession -AllowClobber -DisableNameChecking | Out-Null
                    Write-PSFMessage -Level Output -Message "Connected to Exchange Onprem remotely."
                 }
                catch {}
            } -EnableException $true -PSCmdlet $PSCmdlet
        }

        AD {
            Invoke-PSFProtectedCommand -Action "Connecting to Active Directory." -Target "AD" -ScriptBlock {
                Write-PSFMessage -Level Output -Message "Connecting to Active Directory."
                try {
                    $sessionAD = New-PSSession -ComputerName $env:LogOnServer.Replace("\\","")
                    Invoke-Command { Import-Module ActiveDirectory } -Session $sessionAD
                    Export-PSSession -Session $sessionAD -CommandName *-AD* -OutputModule RemoteAD -AllowClobber -Force | Out-Null
                    Remove-PSSession -Session $sessionAD
                    
                    # Create copy of the module on the local computer
                    Import-Module RemoteAD -Prefix Remote -DisableNameChecking -ErrorAction Stop
                
                } catch {
                    
                    # Sometimes the following path is not registered as system variable for PS modules path, thus we catch explicitly the .psm1
                    Import-Module "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\RemoteAD\RemoteAD.psm1" -Prefix Remote -DisableNameChecking
                        
                } finally {

                    If (Get-Module -Name RemoteAD) {

                        Write-PSFMessage -Level Output -Message "AD Module was succesfully installed."

                    } else {

                        Write-PSFMessage -Level Error -Message "AD module failed to load. Please run the script from an Exchange Server."
                        throw

                    }
                }
            } -EnableException $true -PSCmdlet $PSCmdlet
        }
    }
}

function Get-Requirements {
    <#
    .SYNOPSIS
    Checks requirements
     
    .DESCRIPTION
    Checks requirements
     
    .PARAMETER Requirements
    Lists the available Services requirements to be checked. Currently Available is 'AADConnect'.
     
    .EXAMPLE
    PS C:\> Get-Requirements -Requirements PSFramework, EXO
    Checks if PSFramework and EXO v2 modure is installed. If not, we install it.
    #>


    [CmdletBinding()]
    param (
        [ValidateSet('AADConnect')]
        [String[]]
        $Requirements
    )

    Switch ( $Requirements ) {

        AADConnect {

            $title    = Write-PSFMessage -Level Output -Message  "AD Sync status"
            $question = Write-PSFMessage -Level Output -Message  "Did you stopped the Azure AD Connect sync cycle?"
            $choices  = '&Yes', '&No'
            $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)

            if ($decision -eq 0) {
            
                Write-PSFMessage -Level Output -Message  "Loading parameters..."

            } else {
            
                Write-PSFMessage -Level Output -Message  "AD sync cycle should be stopped before moving forward"
            
                $title1    = Write-PSFMessage -Level Output -Message  ""
                $question1 = Write-PSFMessage -Level Output -Message  "Type 'Yes' if you want that we automatically stop AD Sync cycle or type 'No' if you want to stop yourself"
                $choices1  = '&Yes', '&No'
                $decision1 = $Host.UI.PromptForChoice($title1, $question1, $choices1, 1)

                if ($decision1 -eq 0) {
            
                    $AADC = Read-Host "$(Get-Date -Format "HH:mm:ss") - Please enter the Azure AD Connect server hostname"

                        Write-PSFMessage -Level Output -Message  "Disabling AD Sync cycle..."
                        $sessionAADC = New-PSSession -ComputerName $AADC
                        Invoke-Command {
        
                            Import-Module ADSync
                            Set-ADSyncScheduler -SyncCycleEnabled $false
        
                            } -Session $sessionAADC

                        $SynccycleStatus = Invoke-Command {
        
                            Import-Module ADSync
                            Get-ADSyncScheduler | Select-Object SyncCycleEnabled
                        
                            } -Session $sessionAADC

                            if ($SynccycleStatus.SyncCycleEnabled -eq $false) {
        
                                Write-PSFMessage -Level Output -Message  "Azure AD sync cycle succesfully disabled."
                                $AADCStoped = 1
                                
                            } else {

                                Write-PSFMessage -Level Output -Message  "Azure AD sync cycle could not be stopped, please stop it manually with the following cmdlet: Set-ADSyncScheduler -SyncCycleEnabled 0"
                                $AADCStoped = 0
                                
                            }
                        
                } else {
                    
                    Write-PSFMessage -Level Output -Message  "Please stop the AD sync cycle and run the script again"
                    $AADCStoped = 0
                    
                }
            }
        }
    }

    Get-PSSession | Remove-PSSession
    return $AADCStoped
}



Function Export-T2TAttributes{
    <#
    .SYNOPSIS
    This script will dump all necessary attributes that cross-tenant MRS migration requires.
    No changes will be performed by this code.
     
    .DESCRIPTION
    This script will dump all necessary attributes that cross-tenant MRS migration requires.
    No changes will be performed by this code.
 
    .PARAMETER AdminUPN
    Optional parameter used to connect to to Exchange Online. Only the UPN is
    stored to avoid token expiration during the session, no password is stored.
 
    .PARAMETER CustomAttributeNumber
    Mandatory parameter used to inform the code which custom
    attributes will be used to scope the search. Valid range: 1-15.
 
    .PARAMETER CustomAttributeValue
    Mandatory parameter used to inform the code which value will be used to scope the search.
 
    .PARAMETER DomainMappingCSV
    Enter the CSV name where you mapped the source and target domains
 
    .PARAMETER BypassAutoExpandingArchiveCheck
    The script will check if you have Auto-Expanding archive enable on organization
    level, if yes each mailbox will be check if there is an Auto-Expanding archive mailbox
    This check might increase the script duration. You can opt-out using this switch
 
    .PARAMETER FolderPath
    Optional parameter used to inform which path will be used to save the CSV.
    If no path is chosen, the script will save on the Desktop path.
 
    .PARAMETER LocalMachineIsNotExchange
    Optional parameter used to inform that you are running the script from a
    non-Exchange Server machine. This parameter will require the -ExchangeHostname.
 
    .PARAMETER ExchangeHostname
    Mandatory parameter if the switch -LocalMachineIsNotExchange was used.
    Used to inform the Exchange Server FQDN that the script will connect.
 
    .EXAMPLE
    PS C:\> Export-T2TAttributes -CustomAttributeNumber 10 -CustomAttributeValue "T2T" -DomainMappingCSV sourcetargetmap.csv -FolderPath C:\LoggingPath
    The function will export all users matching the value "T2T" on the CustomAttribute 10, and based on all the users found, we will
    mapping source and target domains according to the CSV provided. All changes and CSV files will be generated in "C:\LoggingPath" folder.
 
    .EXAMPLE
    PS C:\> Export-T2TAttributes -CustomAttributeNumber 10 -CustomAttributeValue "T2T" -DomainMappingCSV sourcetargetmap.csv -FolderPath C:\LoggingPath -LocalMachineIsNotExchange -ExchangeHostname ExServer1
    The function will connect to the onprem Exchange Server "ExServer1" and export all users matching the value
    "T2T" on the CustomAttribute 10, and based on all the users found, we will mapping source and target domains
    according to the CSV provided. All changes and CSV files will be generated in "C:\LoggingPath" folder.
 
    .NOTES
    Title: Export-T2TAttributes.ps1
    Version: 1.2
    Date: 2021.02.04
    Authors: Denis Vilaca Signorelli (denis.signorelli@microsoft.com)
    Contributors: Agustin Gallegos (agustin.gallegos@microsoft.com)
 
    REQUIREMENTS
     
    1 - ExchangeOnlineManagement module (EXO v2)
 
    2 - PSFramework module
 
    3 - To make things easier, run this script from Exchange On-Premises machine powershell,
        the script will automatically import the Exchange On-Prem module. If you don't want
        to run the script from an Exchange machine, use the switch -LocalMachineIsNotExchange
        and enter the Exchange Server hostname.
 
    ##############################################################################################
    #This sample script is not supported under any Microsoft standard support program or service.
    #This sample script is provided AS IS without warranty of any kind.
    #Microsoft further disclaims all implied warranties including, without limitation, any implied
    #warranties of merchantability or of fitness for a particular purpose. The entire risk arising
    #out of the use or performance of the sample script and documentation remains with you. In no
    #event shall Microsoft, its authors, or anyone else involved in the creation, production, or
    #delivery of the scripts be liable for any damages whatsoever (including, without limitation,
    #damages for loss of business profits, business interruption, loss of business information,
    #or other pecuniary loss) arising out of the use of or inability to use the sample script or
    #documentation, even if Microsoft has been advised of the possibility of such damages.
    ##############################################################################################
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding(DefaultParameterSetName="Default")]
    Param(
        [Parameter(Mandatory=$False)]
        [string]$AdminUPN,
        
        [Parameter(Mandatory=$true,
        HelpMessage="Enter the custom attribute number. Valid range: 1-15")]
        [ValidateRange(1,15)]
        [Int]$CustomAttributeNumber,
        
        [Parameter(Mandatory=$true,
        HelpMessage="Enter the custom attribute value that will be used")]
        [string]$CustomAttributeValue,
        
        [Parameter(Mandatory=$true,
        HelpMessage="Enter the CSV name where you mapped the source and target domains")]
        [string]$DomainMappingCSV,
        
        [Parameter(Mandatory=$false)]
        [switch]$BypassAutoExpandingArchiveCheck,

        [Parameter(Mandatory=$false)]
        [string]$FolderPath,
        
        [Parameter(ParameterSetName="RemoteExchange",Mandatory=$false)]
        [switch]$LocalMachineIsNotExchange,
        
        [Parameter(ParameterSetName="RemoteExchange",Mandatory=$true,
        HelpMessage="Enter the remote exchange hostname")]
        [string]$ExchangeHostname
    )
    Set-PSFConfig -FullName PSFramework.Logging.FileSystem.ModernLog -Value $True
    Write-PSFMessage  -Level Output -Message "Starting export script. All logs are being saved in $((Get-PSFConfig PSFramework.Logging.FileSystem.LogPath).Value)"

    if ( $FolderPath -ne '' )
    {
    
    $outFile = "$FolderPath\UserListToImport.csv"
    $AUXFile = "$FolderPath\AUXEnable-Mailboxes.txt"
    
    } else {
    
    $outFile = "$home\desktop\UserListToImport.csv"
    $AUXFile = "$home\desktop\AUXEnable-Mailboxes.txt"
    
    }
    
    $outArray = @()
    $CustomAttribute = "CustomAttribute$CustomAttributeNumber"
    $MappingCSV = Import-CSV -Path $DomainMappingCSV
    
    #Region check current connection status, and connect if needed
    if ( $LocalMachineIsNotExchange.IsPresent ) {
        
        $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeRemote, AD
        # Connect to services if ServicesToConnect is not empty
        if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect -ExchangeHostname $ExchangeHostname}
        # This will make sure when you need to reauthenticate after 1 hour
        # that it uses existing token and you don't have to write password
        $global:UserPrincipalName=$AdminUPN
    
    } else {
        
        $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeLocal
        # Connect to services if ServicesToConnect is not empty
        if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect }
    
    }
    #endregion
    
    # Save all properties from MEU object to variable
    $RemoteMailboxes = Get-RemoteMailbox -resultsize unlimited | Where-Object {$_.$CustomAttribute -like $CustomAttributeValue}
    Write-PSFMessage -Level Output -Message "$($RemoteMailboxes.Count) mailboxes with $($CustomAttribute) as $($CustomAttributeValue) were returned"
    
    
    # Saving AUX org status if bypass switch is not present
    if ( $BypassAutoExpandingArchiveCheck.IsPresent ) {
    
        Write-PSFMessage -Level Output -Message "Bypassing Auto-Expand archive check"
    
    } else {
    
        $OrgAUXStatus = Get-EXOrganizationConfig | Select-Object AutoExpandingArchiveEnabled
    
        if ( $OrgAUXStatus.AutoExpandingArchiveEnabled -eq '$True' ) {
    
            Write-PSFMessage -Level Output -Message "Auto-Expand archive is enabled at organization level"
    
        } else {
    
            Write-PSFMessage -Level Output -Message "Auto-Expand archive is not enabled at organization level, but we will check each mailbox"
    
        }
        
    }
    
    Write-PSFMessage -Level Output -Message "Getting EXO mailboxes necessary attributes. This may take some time..."

    [int]$counter = 0
    Foreach ($i in $RemoteMailboxes)
    {
        
        $counter++
        Write-Progress -Activity "Exporting mailbox attributes to CSV" -Status "Working on $($i.DisplayName)" -PercentComplete ($counter * 100 / $($RemoteMailboxes.Count) )
        
        $user = get-Recipient $i.alias # getting recipients from AD, instead of EXO
        $object = New-Object System.Object
        $object | Add-Member -type NoteProperty -name primarysmtpaddress -value $i.PrimarySMTPAddress
        $object | Add-Member -type NoteProperty -name alias -value $i.alias
        $object | Add-Member -type NoteProperty -name FirstName -value $User.FirstName
        $object | Add-Member -type NoteProperty -name LastName -value $User.LastName
        $object | Add-Member -type NoteProperty -name DisplayName -value $User.DisplayName
        $object | Add-Member -type NoteProperty -name Name -value $i.Name
        $object | Add-Member -type NoteProperty -name SamAccountName -value $i.SamAccountName
        $object | Add-Member -type NoteProperty -name legacyExchangeDN -value $i.legacyExchangeDN
        $object | Add-Member -type NoteProperty -name CustomAttribute -value $CustomAttribute
        $object | Add-Member -type NoteProperty -name CustomAttributeValue -value $CustomAttributeValue
    
        if ( $BypassAutoExpandingArchiveCheck.IsPresent ) {
        
            # Save necessary properties from EXO object to variable avoiding AUX check
            $EXOMailbox = Get-EXOMailbox -Identity $i.Alias -PropertySets Retention,Hold,Archive,StatisticsSeed
        
        } else {

            if ($OrgAUXStatus.AutoExpandingArchiveEnabled -eq '$True') {

                # If AUX is enable at org side, doesn't metter if the mailbox has it explicitly enabled
                $EXOMailbox = Get-EXOMailbox -Identity $i.Alias -PropertySets All | Select-Object ExchangeGuid,MailboxLocations,LitigationHoldEnabled,SingleItemRecoveryEnabled,ArchiveDatabase,ArchiveGuid

            } else {

                # If AUX isn't enable at org side, we check if the mailbox has it explicitly enabled
                $EXOMailbox = Get-EXOMailbox -Identity $i.Alias -PropertySets All | Select-Object ExchangeGuid,MailboxLocations,LitigationHoldEnabled,SingleItemRecoveryEnabled,ArchiveDatabase,ArchiveGuid,AutoExpandingArchiveEnabled
            
            }

        }

        if ( $BypassAutoExpandingArchiveCheck.IsPresent ) {
        
            # Save necessary properties from EXO object to variable avoiding AUX check
            Write-PSFMessage -Level Output -Message "Bypassing MailboxLocation check for Auto-Expand archive"

        } else {

            # AUX enabled doesn't mean that the mailbox indeed have AUX
            # archive. We need to check the MailboxLocation to be sure
            if ( ($OrgAUXStatus.AutoExpandingArchiveEnabled -eq '$True' -and $EXOMailbox.MailboxLocations -like '*;AuxArchive;*') -or
            ($OrgAUXStatus.AutoExpandingArchiveEnabled -eq '$False' -and $EXOMailbox.AutoExpandingArchiveEnabled -eq '$True' -and
            $EXOMailbox.MailboxLocations -like '*;AuxArchive;*') )
            {

                $AuxMessage = "[$(Get-Date -format "HH:mm:ss")] User $($i.Alias) has an auxiliar Auto-Expanding archive mailbox. Be aware that any auxiliar archive mailbox will not be migrated"
                $AuxMessage | Out-File -FilePath $AUXFile -Append
                Write-PSFHostColor -String $AuxMessage -DefaultColor Cyan
                
            }
        }

        # Get mailbox guid from EXO because if the mailbox was created from scratch
        # on EXO the ExchangeGuid would not be write-backed to On-Premises
        $object | Add-Member -type NoteProperty -name ExchangeGuid -value $EXOMailbox.ExchangeGuid
        
        # Get mailbox ECL value
        $ELCValue = 0
        if ($EXOMailbox.LitigationHoldEnabled) {$ELCValue = $ELCValue + 8}
        if ($EXOMailbox.SingleItemRecoveryEnabled) {$ELCValue = $ELCValue + 16}
        if ($ELCValue -ge 0) { $object | Add-Member -type NoteProperty -name ELCValue -value $ELCValue}
        
        # Get the ArchiveGuid from EXO if it exist. The reason that we don't rely on
        # "-ArchiveStatus" parameter is that may not be trustable in certain scenarios
        # https://docs.microsoft.com/en-us/office365/troubleshoot/archive-mailboxes/archivestatus-set-none
        if ( $EXOMailbox.ArchiveDatabase -ne '' -and
                $EXOMailbox.ArchiveGuid -ne "00000000-0000-0000-0000-000000000000" )
        {
            
            $object | Add-Member -type NoteProperty -name ArchiveGuid -value $EXOMailbox.ArchiveGuid
        
        } else {

            $object | Add-Member -type NoteProperty -name ArchiveGuid -value $Null

        }

        # Get any SMTP alias avoiding *.onmicrosoft
        $ProxyArray = @()
        $TargetArray = @()
        $Proxy = $i.EmailAddresses
        foreach ($email in $Proxy)
        {
            if ($email -notlike '*.onmicrosoft.com')
            {

                $ProxyArray = $ProxyArray += $email

            }
            if ($email -like '*.onmicrosoft.com')
            {

                $TargetArray = $TargetArray += $email

            }
        }
                
        # Join it using ";" and replace the old domain (source) to the new one (target)
        $ProxyToString = $ProxyArray -Join ";"

        # Map from the CSV which source domain will become which source domain
        Foreach ($Domain in $MappingCSV) {

            # Add @ before the domain to avoid issues with subdomains
            $SourceDomain = $Domain.Source.Insert(0,"@")
            $TargetDomain = $Domain.Target.Insert(0,"@")

            if ($ProxyToString -match $Domain.source) {

                $ProxyToString = $ProxyToString -replace $SourceDomain,$TargetDomain

            }

        }

        $object | Add-Member -type NoteProperty -name EmailAddresses -value $ProxyToString

        # Get ProxyAddress only for *.mail.onmicrosoft to define in the target AD the targetAddress value
        $TargetToString = [system.String]::Join(";",$TargetArray)
        $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $TargetToString.Replace("smtp:","")


        if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '')
        {

            # Connect to AD exported module only if this machine isn't an Exchange
            $Junk = Get-RemoteADUser -Identity $i.SamAccountName -Properties *
        
        } else {

            $Junk = Get-ADUser -Identity $i.SamAccountName -Properties *

        }

        # Get Junk hashes, these are SHA-265 write-backed from EXO. Check if the user
        # has any hash, if yes we convert the HEX to String removing the "-"
        if ( $junk.msExchSafeSendersHash.Length -gt 0 )
        {
        
            $SafeSender = [System.BitConverter]::ToString($junk.msExchSafeSendersHash)
            $Safesender = $SafeSender.Replace("-","")
            $object | Add-Member -type NoteProperty -name SafeSender -value $SafeSender
        
        } else {

            $object | Add-Member -type NoteProperty -name SafeSender $Null
        
        }
        
        if ( $junk.msExchSafeRecipientsHash.Length -gt 0 )
        {
            
            $SafeRecipient = [System.BitConverter]::ToString($junk.msExchSafeRecipientsHash)
            $SafeRecipient = $SafeRecipient.Replace("-","")
            $object | Add-Member -type NoteProperty -name SafeRecipient -value $SafeRecipient

        }  else {

            $object | Add-Member -type NoteProperty -name SafeRecipient -value $Null

        }

        if ( $junk.msExchBlockedSendersHash.Length -gt 0 )
        {
            
            $BlockedSender = [System.BitConverter]::ToString($junk.msExchBlockedSendersHash)
            $BlockedSender = $BlockedSender.Replace("-","")
            $object | Add-Member -type NoteProperty -name BlockedSender -value $BlockedSender
        
        } else {

            $object | Add-Member -type NoteProperty -name BlockedSender -value $Null

        }

        $outArray += $object
    
    }
    
    # Export to a CSV and clear up variables and sessions
    if ( $BypassAutoExpandingArchiveCheck.IsPresent ) {
        
        Write-PSFMessage -Level Output -Message  "Saving CSV on $($outfile)"
    
    } else {
    
        Write-PSFMessage -Level Output -Message "Saving CSV on $($outfile)"
        Write-PSFMessage -Level Output -Message "Saving TXT on $($AUXFile)"
    
    }
    
    $outArray | Export-CSV $outfile -notypeinformation
    Remove-Variable * -ErrorAction SilentlyContinue
    Get-PSSession | Remove-PSSession
    Disconnect-ExchangeOnline -Confirm:$false

}

function Export-T2TLogs
{
    <#
    .SYNOPSIS
    This function will export current PSFramework logs.
     
    .DESCRIPTION
    This function will export current PSFramework logs based on the amount of days old define in the 'DaysOld' parameter.
    It will allow to export to CSV file and/or display in powershell GridView.
    Output will have the following header: "ComputerName","Username","Timestamp","Level","Message","Type","FunctionName","ModuleName","File","Line","Tags","TargetObject","Runspace","Callstack"
     
    .PARAMETER FilePath
    Defines the path file to export the CSV file.
    Default value is the user's Desktop with a file name like "yyyy-MM-dd HH_mm_ss" - T2T logs.csv"
     
    .PARAMETER OutputType
    Defines the output types available. Can be a single output or combined.
    Current available options are CSV, GridView.
 
    .PARAMETER DaysOld
    Defines how old we will go to fetch the logs. Valid range is between 1 through 7 days old. Default Value is 1
     
    .EXAMPLE
    PS C:\> Export-T2Tlogs -OutputType CSV
    In this example, the script will fetch all logs within the last 24 hrs (by default), and export to CSV to default location at the Desktop.
     
    .EXAMPLE
    PS C:\> Export-T2Tlogs -OutputType GridView -DaysOld 3
    In this example, the script will fetch all logs within the last 3 days, and displays them in powershell's GridView.
 
    .EXAMPLE
    PS C:\> Export-T2Tlogs -OutputType CSV,GridView -DaysOld 5
    In this example, the script will fetch all logs within the last 5 days, export to CSV to default location at the Desktop and also displays in powershell's GridView.
 
    .EXAMPLE
    PS C:\> Export-T2Tlogs -OutputType CSV,GridView -DaysOld 7 -FilePath "C:\Temp\newLog.csv"
    In this example, the script will fetch all logs within the last 7 days, export to CSV to path "C:\Temp\newLog.csv" and also displays them in powershell's GridView.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    Param (
        [String]$FilePath = "$home\Desktop\$(get-date -Format "yyyy-MM-dd HH_mm_ss") - T2T logs.csv",
        
        [ValidateSet('CSV','GridView')]
        [string[]]$OutputType = "GridView",

        [ValidateRange(1,7)]
        [int]$DaysOld = 1
    )
    Import-module PSFramework
    $loggingpath = (Get-PSFConfig PSFramework.Logging.FileSystem.LogPath).Value
    $logFiles = Get-ChildItem -Path $loggingpath | Where-Object LastwriteTime -gt (Get-Date).adddays(-$DaysOld)
    $csv = Import-Csv -Path $logFiles.FullName
    Switch ( $OutputType)
    {
        CSV { $csv | Sort-Object timestamp | export-csv -Path $FilePath -NoTypeInformation }
        GridView { $csv | Sort-Object timestamp | Out-GridView }
    }
}


Function Import-T2TAttributes {
    <#
    .SYNOPSIS
    The script will create on the target AD On-Prem the MEU objects getting all attribute values from the CSV generated by Export-T2TAttributes command.
     
    .DESCRIPTION
    The script will create on the target AD On-Prem the MEU objects getting all attribute values from the CSV generated by Export-T2TAttributes command.
 
    .PARAMETER UPNSuffix
    Mandatory parameter used to inform which is the UPN domain for the MEU object e.g: contoso.com
 
    .PARAMETER Password
    Optional parameter if you want to choose a password for all new MEU objects
 
    .PARAMETER ResetPassword
    Optional parameter if you want to require users to reset password in the first sign-in
 
    .PARAMETER OU
    Optional parameter if you want to create MEU objects in a specific OU. Valid values are
    name, Canonical name, Distinguished name (DN) or GUID. If not defined, the user object will
    be created on Users container.
 
    .PARAMETER FilePath
    Optional parameter used to inform which path will be used import the CSV. If no
    path is chosen, the script will search for UserListToImport.csv file on desktop path.
 
    .PARAMETER LocalMachineIsNotExchange
    Optional parameter used to inform that you are running the script from a
    non-Exchange Server machine. This parameter will require the -ExchangeHostname.
 
    .PARAMETER ExchangeHostname
    Mandatory parameter if the switch -LocalMachineIsNotExchange was used.
    Used to inform the Exchange Server FQDN that the script will connect.
 
    .EXAMPLE
    PS C:\> Import-T2TAttributes -UPNSuffix "fabrikam.com" -ResetPassword -FilePath "C:\temp\UserListToImport.csv"
    The function will import all users from the file "C:\temp\UserListToImport.csv", create the new MailUsers
    with the new UPNSuffix of "fabrikam.com", and enable the check mark to "Reset the password on next logon".
 
    .EXAMPLE
    PS C:\> Import-T2TAttributes -UPNSuffix "fabrikam.com" -ResetPassword -FilePath "C:\temp\UserListToImport.csv" -LocalMachineIsNotExchange -ExchangeHostname "ExServer2"
    The function will connect to the onprem Exchange Server "ExServer2" and import all users
    from the file "C:\temp\UserListToImport.csv", create the new MailUsers with the new UPNSuffix
    of "fabrikam.com", and enable the check mark to "Reset the password on next logon".
 
    .NOTES
    Title: Import-T2TAttributes.ps1
    Version: 1.2
    Date: 2021.01.03
    Author: Denis Vilaca Signorelli (denis.signorelli@microsoft.com)
    Contributors: Agustin Gallegos (agustin.gallegos@microsoft.com)
 
 
    REQUIREMENTS:
     
    1 - To make things easier, run this script from Exchange On-Premises machine powershell,
        the script will automatically import the Exchange On-Prem module. If you don't want
        to run the script from an Exchange machine, use the switch -LocalMachineIsNotExchange
        and enter the Exchange Server hostname.
 
    2 - The script encourage you to stop the Azure AD Sync cycle before the execution. The
        script can disable the sync for you as long as you provide the Azure AD Connect
        hostname. Otherwiser, you can disable by your self manually and then re-run the script.
 
    ##############################################################################################
    #This sample script is not supported under any Microsoft standard support program or service.
    #This sample script is provided AS IS without warranty of any kind.
    #Microsoft further disclaims all implied warranties including, without limitation, any implied
    #warranties of merchantability or of fitness for a particular purpose. The entire risk arising
    #out of the use or performance of the sample script and documentation remains with you. In no
    #event shall Microsoft, its authors, or anyone else involved in the creation, production, or
    #delivery of the scripts be liable for any damages whatsoever (including, without limitation,
    #damages for loss of business profits, business interruption, loss of business information,
    #or other pecuniary loss) arising out of the use of or inability to use the sample script or
    #documentation, even if Microsoft has been advised of the possibility of such damages.
    ##############################################################################################
 
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlaintextForPassword", "")]
    [CmdletBinding(DefaultParameterSetName="Default")]
    Param(
        [Parameter(Mandatory=$true,
        HelpMessage="Enter UPN suffix of your domain E.g. contoso.com")]
        [string]$UPNSuffix,
        
        [Parameter(Mandatory=$false,
        HelpMessage="Enter the password for the new MEU objects. If no password is chosen,
        the script will define '?r4mdon-_p@ss0rd!' as password"
)]
        [string]$Password,
        
        [Parameter(Mandatory=$false,
        HelpMessage="Require password change on first user access")]
        [switch]$ResetPassword,
        
        [Parameter(Mandatory=$false,
        HelpMessage="Enter the organization unit that MEU objects will be created.
        The input is accepted as Name, Canonical name, Distinguished name (DN) or GUID"
)]
        [string]$OU,
        
        [Parameter(Mandatory=$false,
        HelpMessage="Enter a custom import path for the csv. if no value is defined
        the script will search on Desktop path for the UserListToImport.csv"
)]
        [string]$FilePath,

        [Parameter(ParameterSetName="RemoteExchange",Mandatory=$false)]
        [switch]$LocalMachineIsNotExchange,
        
        [Parameter(ParameterSetName="RemoteExchange",Mandatory=$true,
        HelpMessage="Enter the remote exchange hostname")]
        [string]$ExchangeHostname
    )
    Set-PSFConfig -FullName PSFramework.Logging.FileSystem.ModernLog -Value $True
    Write-PSFMessage  -Level Output -Message "Starting script. All logs are being saved in: $((Get-PSFConfig PSFramework.Logging.FileSystem.LogPath).Value)"

    # Requirements check
    $AADCStopped =  Get-Requirements -Requirements AADConnect
    if ( $AADCStopped -eq 0 ) { Break }

    if ( $Password -ne '' ) {
        
        $pwstr = $Password

    } else {

        $pwstr = "?r4mdon-_p@ss0rd!"

    }

    if ( $FilePath -ne '' ) {
        
        $ImportUserList = Import-CSV "$FilePath"

    } else {

        $ImportUserList = Import-CSV "$home\desktop\UserListToImport.csv"

    }

    if ( $ResetPassword.IsPresent ) {

        [bool]$resetpwrd = $True

    } else {

        [bool]$resetpwrd = $False

    }


    $UPNSuffix = "@$UPNSuffix"
    $pw = new-object "System.Security.SecureString";
    #$CustomAttribute = "CustomAttribute$CustomAttributeNumber"

    # Connecto to Exchange and AD
    if ( $LocalMachineIsNotExchange ) {

        $ServicesToConnect = Assert-ServiceConnection -Services AD, ExchangeRemote
        # Connect to services if ServicesToConnect is not empty
        if ( $ServicesToConnect.Count ) { Connect-OnlineServices -Services $ServicesToConnect -ExchangeHostname $ExchangeHostname }
    
    } else {

        $ServicesToConnect = Assert-ServiceConnection -Services ExchangeLocal
        # Connect to services if ServicesToConnect is not empty
        if ( $ServicesToConnect.Count ) { Connect-OnlineServices -Services $ServicesToConnect }
    }

    for ($i=0; $i -lt $pwstr.Length; $i++) {$pw.AppendChar($pwstr[$i])}

    [int]$counter = 0
    foreach ($user in $ImportUserList)
    {
        $counter++
        Write-Progress -Activity "Creating MEU objects and importing attributes from CSV" -Status "Working on $($user.DisplayName)" -PercentComplete ($counter * 100 / $($ImportUserList.Count) )
        
        $tmpUser = $null
            
        $UPN = $user.Alias+$UPNSuffix
        
        # If OU was passed through param, honor it.
        # Otherwise create the MEU without OU specification
        if ( $OU -ne '' -or $OU -ne $Null)
        {
            $tmpUser = New-MailUser -UserPrincipalName $upn -ExternalEmailAddress $user.ExternalEmailAddress -FirstName $user.FirstName -LastName $user.LastName -SamAccountName $user.SamAccountName -Alias $user.alias -PrimarySmtpAddress $UPN -Name $User.Name -DisplayName $user.DisplayName -Password $pw -ResetPasswordOnNextLogon $resetpwrd -OrganizationalUnit $OU

        } else {

            $tmpUser = New-MailUser -UserPrincipalName $upn -ExternalEmailAddress $user.ExternalEmailAddress -FirstName $user.FirstName -LastName $user.LastName -SamAccountName $user.SamAccountName -Alias $user.alias -PrimarySmtpAddress $UPN -Name $User.Name -DisplayName $user.DisplayName -Password $pw -ResetPasswordOnNextLogon $resetpwrd

        }

        # Convert legacyDN to X500, replace back to ","
        $x500 = "x500:" + $user.legacyExchangeDN
        $proxy = $user.EmailAddresses.Replace(";",",")
        $ProxyArray = @()
        $ProxyArray = $Proxy -split ","
        $ProxyArray = $ProxyArray + $x500
        
        # Matching the variable's name to the parameter's name
        $CustomAttributeParam = @{ $User.CustomAttribute = $user.CustomAttributeValue }
        
        # Set ExchangeGuid, old LegacyDN as X500 and CustomAttribute
        $tmpUser | Set-MailUser -ExchangeGuid $user.ExchangeGuid @CustomAttributeParam -EmailAddresses @{ Add=$ProxyArray }

        # Set ELC value
        if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' )
        {
            
            Set-RemoteADUser -Identity $user.SamAccountName -Replace @{ msExchELCMailboxFlags = $user.ELCValue }
                
        } else {

            Set-ADUser -Identity $user.SamAccountName -Replace @{ msExchELCMailboxFlags=$user.ELCValue }
                
        }
                        
        # Set ArchiveGuid if user has source cloud archive. We don't really care if the
        # archive will be moved, it's up to the batch to decide, we just sync the attribute
        if ( $null -ne $user.ArchiveGuid -and $user.ArchiveGuid -ne '' )
        {
            
            $tmpUser | Set-MailUser -ArchiveGuid $user.ArchiveGuid
        
        }

        # If the user has Junk hash, convert the HEX string to byte array and set it
        if ( $null -ne $user.SafeSender -and $user.SafeSender -ne '' )
        {
        
            $BytelistSafeSender = New-Object -TypeName System.Collections.Generic.List[System.Byte]
            $HexStringSafeSender = $user.SafeSender
                for ($i = 0; $i -lt $HexStringSafeSender.Length; $i += 2)
                {
                    $HexByteSafeSender = [System.Convert]::ToByte($HexStringsafeSender.Substring($i, 2), 16)
                    $BytelistSafeSender.Add($HexByteSafeSender)
                }
            
            $BytelistSafeSenderArray = $BytelistSafeSender.ToArray()
            
                if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' )
                {
                    
                    Set-RemoteADUser -Identity $user.SamAccountName -Replace @{ msExchSafeSendersHash = $BytelistSafeSenderArray }

                } else {

                    Set-ADUser -Identity $user.SamAccountName -Replace @{ msExchSafeSendersHash = $BytelistSafeSenderArray }

                }
            
        }

        if ( $null -ne $user.SafeRecipient -and $user.SafeRecipient -ne '' )
        {
        
            $BytelistSafeRecipient = New-Object -TypeName System.Collections.Generic.List[System.Byte]
            $HexStringSafeRecipient = $user.SafeRecipient
                for ($i = 0; $i -lt $HexStringSafeRecipient.Length; $i += 2)
                {
                    $HexByteSafeRecipient = [System.Convert]::ToByte($HexStringSafeRecipient.Substring($i, 2), 16)
                    $BytelistSafeRecipient.Add($HexByteSafeRecipient)
                }
            
            $BytelistSafeRecipientArray = $BytelistSafeRecipient.ToArray()
            
                if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' )
                {
                    
                    Set-RemoteADUser -Identity $user.SamAccountName -Replace @{ msExchSafeRecipientsHash = $BytelistSafeRecipientArray }

                } else {

                    Set-ADUser -Identity $user.SamAccountName -Replace @{ msExchSafeRecipientsHash = $BytelistSafeRecipientArray }

                }
        
        }

        if ( $null -ne $user.BlockedSender -and $user.BlockedSender -ne '' )
        {
        
            $BytelistBlockedSender = New-Object -TypeName System.Collections.Generic.List[System.Byte]
            $HexStringBlockedSender = $user.BlockedSender
                for ($i = 0; $i -lt $HexStringBlockedSender.Length; $i += 2)
                {
                    $HexByteBlockedSender = [System.Convert]::ToByte($HexStringBlockedSender.Substring($i, 2), 16)
                    $BytelistBlockedSender.Add($HexByteBlockedSender)
                }
            
            $BytelistBlockedSenderArray = $BytelistBlockedSender.ToArray()
            
                if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' )
                {
                    
                    Set-RemoteADUser -Identity $user.SamAccountName -Replace @{ msExchBlockedSendersHash = $BytelistBlockedSenderArray }

                } else {

                    Set-ADUser -Identity $user.SamAccountName -Replace @{ msExchBlockedSendersHash = $BytelistBlockedSenderArray }

                }
        
            }
        Write-PSFMessage -Level InternalComment -Message "$($user.alias) MailUser successfully created."
    }

    Write-PSFMessage -Level Output -Message "The import is completed. Please confirm that all users are correctly created before enable the Azure AD Sync Cycle."
    Write-PSFMessage -Level Output -Message "You can re-enable Azure AD Connect using the following cmdlet: 'Set-ADSyncScheduler -SyncCycleEnabled 1'"
    Remove-Variable * -ErrorAction SilentlyContinue
    Get-PSSession | Remove-PSSession

}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'T2Tscripts' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'T2Tscripts' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'T2Tscripts' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'T2Tscripts.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "T2Tscripts.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name T2Tscripts.alcohol
#>


New-PSFLicense -Product 'T2Tscripts' -Manufacturer 'designor' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2021-02-04") -Text @"
Copyright (c) 2021 designor
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code