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]$Global: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-CSVStatus { <# .SYNOPSIS Checks CSV Status .DESCRIPTION This functions is used to import CSV and check if the CSV was successfully imported. .PARAMETER User Import UserListToImport.csv and check if the file was successfully imported .PARAMETER UsersMigrated Import UsersMigrated.csv and check if the file was successfully imported .PARAMETER Contact Import ContactListToImport.csv and check if the file was successfully imported .PARAMETER MappingFile Import the domain mapping file and check if the file was successfully imported .EXAMPLE PS C:\> Get-CSVStatus -MappingFile Import the CSV mapping file and provide a return to the main function if the file was successfully imported. #> [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [switch]$User, [Parameter(Mandatory=$false)] [switch]$UsersMigrated, [Parameter(Mandatory=$false)] [switch]$Contact, [Parameter(Mandatory=$false)] [switch]$MappingFile ) if ( $User.IsPresent ) { $UserListToImportCheck = 1 if ( $UserListToImport ) { Try { $global:ImportUserList = Import-CSV "$UserListToImport" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The UserListToImport.csv successfully imported." } catch { $UserListToImportCheck = 0 Write-PSFMessage -Level Output -Message "The UserListToImport.csv does not exist. Please import a valid CSV file e re-run the function." } } else { Try { $global:ImportUserList = Import-CSV "$home\desktop\UserListToImport.csv" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The UserListToImport.csv successfully imported from $($home)\Desktop." } catch { $UserListToImportCheck = 0 Write-PSFMessage -Level Output -Message "The UserListToImport.csv was not found in $($home)\Desktop'. Please import a valid CSV file e re-run the function." } } return $UserListToImportCheck } if ( $UsersMigrated.IsPresent ) { $MigratedUsersImportCheck = 1 if ( $MigratedUsers ) { Try { $Global:updatelist = Import-CSV "$MigratedUsers" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The MigratedUsers.csv successfully imported." } catch { $MigratedUsersImportCheck = 0 Write-PSFMessage -Level Output -Message "The MigratedUsers.csv does not exist. Please import a valid CSV file e re-run the function." } } else { Try { $Global:updatelist = Import-CSV "$home\desktop\MigratedUsers.csv" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The MigratedUsers.csv successfully imported from $($home)\Desktop." } catch { $MigratedUsersImportCheck = 0 Write-PSFMessage -Level Output -Message "The MigratedUsers.csv was not found in $($home)\Desktop'. Please import a valid CSV file e re-run the function." } } return $MigratedUsersImportCheck } if ( $Contact.IsPresent ) { $ContactListToImportCheck = 1 if ( $ContactListToImport ) { Try { $global:ImportContactList = Import-CSV "$ContactListToImport" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The ContactListToImport.csv successfully imported." $ContactListToImportCheck = 1 } catch { $ContactListToImportCheck = 0 Write-PSFMessage -Level Output -Message "The ContactListToImport.csv does not exist. Please import a valid CSV file e re-run the function." } } else { Try { $global:ImportContactList = Import-CSV "$home\desktop\ContactListToImport.csv" -ErrorAction Stop Write-PSFMessage -Level Output -Message "The ContactListToImport.csv successfully imported from $($home)\Desktop." $ContactListToImportCheck = 1 } catch { $ContactListToImportCheck = 0 Write-PSFMessage -Level Output -Message "ContactListToImport.csv was not found in $($home)\Desktop'. No Mail Contact will be imported." } } return $ContactListToImportCheck } if ( $MappingFile.IsPresent ) { $CSVMappingExist = 1 Try { Import-CSV -Path $DomainMappingCSV -ErrorAction Stop } catch { $CSVMappingExist = 0 Write-PSFMessage -Level Output -Message "The Domain File Mapping does not exist. Please import a valid CSV file e re-run the function." if ($CSVMappingExist) { return $CSVMappingExist } } return $CSVMappingExist } } 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 { $AADCStoped = 0 Write-PSFMessage -Level Output -Message "Please stop the AD sync cycle and run the script again." } } Get-PSSession | Remove-PSSession } } return $AADCStoped } Function Import-Manager { <# .SYNOPSIS Import Manager Attribute .DESCRIPTION Function called by Import-T2TAttributes if we found the Manager property on the UserListToImport.csv and/or ContactsListToImport.csv .PARAMETER ObjType Type of object that the function will work, valid values are MEU or Contact .EXAMPLE PS C:\> Import-Manager -ObjType MEU, Contacts Import the manager attribute valies from from the UserListToImport.csv and ContactsListToImport.csv #> [CmdletBinding()] param ( [ValidateSet('MEU','Contact')] [String]$ObjType ) Switch ( $ObjType ) { MEU { Write-PSFMessage -Level Output -Message "Starting Manager attribute import" [int]$counter = 0 $ManagerCount = ($ImportUserList | Measure-Object).count foreach ( $i in $ImportUserList ) { $counter++ Write-Progress -Activity "Importing Manager Attribute" -Status "Working on $($i.DisplayName)" -PercentComplete ($counter * 100 / $ManagerCount ) if ( $LocalMachineIsNotExchange.IsPresent -and $i.Manager ) { Try { Set-RemoteADUser -Identity $i.SamAccountName -Manager $i.Manager -ErrorAction Stop } catch { Write-PSFMessage -Level Output -Message "Failed to add the user's $($i.DisplayName) manager attribute" } } elseif ( $i.Manager ) { Try { Set-ADUser -Identity $i.SamAccountName -Manager $i.Manager -ErrorAction Stop } catch { Write-PSFMessage -Level Output -Message "Failed to add the user's $($i.DisplayName) manager attribute" } } } } Contact { Write-PSFMessage -Level Output -Message "MailContacts - Starting manager attribute import" [int]$counter = 0 $ManagerCount = ($ImportContactList | Measure-Object).count foreach ( $i in $ImportContactList ) { $counter++ Write-Progress -Activity "MailContacts - Importing Manager Attribute" -Status "Working on $($i.DisplayName)" -PercentComplete ($counter * 100 / $ManagerCount ) Try { Set-Contact -Identity $i.Alias -Manager $i.Manager -ErrorAction Stop } catch { Write-PSFMessage -Level Output -Message "MailContacts - Failed to add the user's $($i.DisplayName) manager attribute" } } } } } Function Move-Contacts { <# .SYNOPSIS Function to handle export and import of mail enable contacts .PARAMETER Sync Decide to perform the export or import of mail contacts .DESCRIPTION Similar to the Export-T2TAttributes, this function dumps attributes from the source AD but only External Contacts. We rely on the same CustomAttributed passed through Export-T2TAttributes to filter which contacts will be fetched by this function. From the Import-T2TAttributes user must pass through param the CSV to import the mail contacts. .EXAMPLE PS C:\> Move-Contacts -Sync Export The cmdlet above perform an export of mail contacts filtered by the custom attribute chosen. #> [CmdletBinding()] param ( [ValidateSet('Export','Import')] [String[]] $Sync ) Switch ( $Sync ) { Export { # region variables $outArray = @() $outFile = "$home\desktop\ContactListToImport.csv" $ContactCustomAttribute = $CustomAttribute $ContactCustomAttributeValue = $CustomAttributeValue # Get Contacts filtering by custom attribute $Contacts = Get-MailContact -resultsize unlimited | Where-Object { $_.$ContactCustomAttribute -like $ContactCustomAttributeValue } Write-PSFMessage -Level Output -Message "$($Contacts.Count) Mail Contacts with $($ContactCustomAttribute) as $($ContactCustomAttributeValue) were returned" [int]$counter = 0 $ContactCount = ($Contacts | Measure-Object).count Foreach ( $i in $Contacts ) { $counter++ Write-Progress -Activity "Exporting Mail Contacts to CSV" -Status "Working on $($i.DisplayName)" -PercentComplete ( $counter * 100 / $ContactCount ) $user = get-Recipient $i.alias $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 legacyExchangeDN -value $i.legacyExchangeDN $object | Add-Member -type NoteProperty -name CustomAttribute -value $ContactCustomAttribute $object | Add-Member -type NoteProperty -name CustomAttributeValue -value $ContactCustomAttributeValue # ExternalEmailAddress should contains "SMTP:" depending # on the deserialization, just try a replace to avoid IF $j = $i.ExternalEmailAddress -replace "SMTP:","" $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $j # Get manager property and resolve CN to alias if ( $IncludeManager.IsPresent -and $user.Manager -ne $Null ) { $Manager = ( Get-Recipient $user.Manager ).Alias $object | Add-Member -type NoteProperty -name Manager -value $Manager } if ( $IncludeManager.IsPresent -and $user.Manager -eq $Null ) { $object | Add-Member -type NoteProperty -name Manager -value $Null } # Get only non-primary smtp and X500 from proxyAddresses. If we get the primary # the CSV mapping domain logic will break as SMTP should be external for contacts $ProxyArray = @() $Proxy = $i.EmailAddresses foreach ($email in $Proxy) { if ( $email -clike 'smtp:*' -or $email -like 'x500:*' -and $email -notlike '*.onmicrosoft.com' ) { $ProxyArray = $ProxyArray += $email } } # Join it using ";" $ProxyToString = $ProxyArray -Join ";" # Map from the CSV which source domain will become which target 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 $outArray += $object } Write-PSFMessage -Level Output -Message "Saving CSV on $($outfile)" $outArray | Export-CSV $outfile -notypeinformation } Import { [int]$counter = 0 $ContactsCount = ( $ImportContactList | Measure-Object ).count foreach ( $i in $ImportContactList ) { $counter++ Write-Progress -Activity "Creating MEU objects and importing attributes from CSV" -Status "Working on $($i.DisplayName)" -PercentComplete ( $counter * 100 / $ContactsCount ) $tmpContact = $null # If OU was passed through param, honor it. # Otherwise create the MEU without OU specification if ( $OUContacts ) { $tmpContact = New-MailContact -ExternalEmailAddress $i.ExternalEmailAddress -PrimarySmtpAddress ` $i.PrimarySMTPAddress -FirstName $i.FirstName -LastName $i.LastName -Alias $i.alias -Name ` $i.Name -DisplayName $i.DisplayName -OrganizationalUnit $OUContacts } else { $tmpContact = New-MailContact -ExternalEmailAddress $i.ExternalEmailAddress -PrimarySmtpAddress ` $i.PrimarySMTPAddress -FirstName $i.FirstName -LastName $i.LastName -Alias $i.alias -Name ` $i.Name -DisplayName $i.DisplayName } # Convert legacyDN to X500, replace back to "," $x500 = "x500:" + $i.legacyExchangeDN $proxy = $i.EmailAddresses.Replace(";",",") $ProxyArray = @() $ProxyArray = $Proxy -split "," $ProxyArray = $ProxyArray + $x500 # Matching the variable's name to the parameter's name $CustomAttributeParam = @{ $i.CustomAttribute=$i.CustomAttributeValue } # Set previou LegacyDN as X500 and CustomAttribute Set-MailContact -Identity $i.Alias -EmailAddresses @{ Add=$ProxyArray } @CustomAttributeParam } # Import Manager value if the CSV contains the manager header $IncludeManager = $ImportContactList[0].psobject.Properties | Where { $_.Name -eq "Manager" } if ( $IncludeManager ) { Import-Manager -ObjType Contact } } } } Function Update-Source { <# .SYNOPSIS Function to convert RemoteMailbox to MailUser and update the targetAddress to destination. .DESCRIPTION This function is called by Update-T2TPostMigration function through -Source param. Basically we need to save a bunch of Exchange attributes before the convertion, as there is no easy way to do that in a supported we must disable the remotemailbox and enable the MEU, but doing so all Exchange attributes will be deleted so the function needs to re-assign. .EXAMPLE PS C:\> Update-Source The following example run this function. #> # region local variables $MigratedUsersImportCheck = Get-CSVStatus -UsersMigrated if ( $MigratedUsersImportCheck -eq 0 ) { Break } $UsersCount = ($updatelist | Measure-Object).count [int]$counter = 0 $Properties = @( 'extensionAttribute1' 'extensionAttribute2' 'extensionAttribute3' 'extensionAttribute4' 'extensionAttribute5' 'extensionAttribute6' 'extensionAttribute7' 'extensionAttribute8' 'extensionAttribute9' 'extensionAttribute10' 'extensionAttribute11' 'extensionAttribute12' 'extensionAttribute13' 'extensionAttribute14' 'extensionAttribute15' ) # region loop each user foreach ( $i in $updatelist ) { $counter++ Write-Progress -Activity "Converting RemoteMailbox to MEU and changing ExternalEmailAddress" -Status "Working on $($i.ExternalEmailAddress)" -PercentComplete ( $counter * 100 / $UsersCount ) if ( $i.MoveRequestStatus -eq 'Completed' ) { # save user properties to variable before disable remote mailbox try { $user = Get-RemoteMailbox -Identity $i.Alias -ErrorAction Stop if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' ) { $aduser = Get-RemoteADUser $i.Alias -Properties * | Select-Object -Property $Properties -ErrorAction Stop } else { $aduser = Get-ADUser $i.Alias -Properties * | Select-Object -Property $Properties -ErrorAction Stop } } catch { Write-PSFMessage -Level Output -Message "The RemoteMailbox $($i.PrimarySMTPAddress) was not found." } # disable remote mailbox if ( $user ) { Disable-RemoteMailbox -Identity $i.Alias -Confirm:$false if ( $? ) { Write-PSFMessage -Level Output -Message "RemoteMailbox $($i.PrimarySMTPAddress) successfully converted to MailUser." # if -UsePrimarySMTPAsTargetAddress is preset, # honor it, otherwise set MOERA as targetAddress if ( $UsePrimarySMTPAsTargetAddress.IsPresent ) { Enable-MailUser -Identity $i.Alias -ExternalEmailAddress $i.PrimarySMTPAddress | Out-Null } else { Enable-MailUser -Identity $i.Alias -ExternalEmailAddress $i.ExternalEmailAddress | Out-Null } # convert legacyDN to X500 and set proxyAddresses $x500 = "x500:" + $user.legacyExchangeDN $proxy = $user.EmailAddresses $ProxyArray = @() $ProxyArray = $Proxy -split "," $ProxyArray = $ProxyArray + $x500 Set-MailUser -Identity $i.Alias -EmailAddresses @{ Add = $ProxyArray } -HiddenFromAddressListsEnabled $user.HiddenFromAddressListsEnabled # if there were custom attribues before, re-add them $Replace = @{} foreach ( $element in $Properties ) { if ( $aduser.$element ) { $Replace.Add( $element, $aduser.$element ) } } if ( $Replace ) { Set-ADUser -Identity $i.Alias -Replace $Replace } } } } } # region clean up variables and sessions Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue Get-PSSession | Remove-PSSession Remove-Variable * -ErrorAction SilentlyContinue } Function Update-Target { <# .SYNOPSIS Function to update the targetAddress and object type from MailUser to RemoteMailbox .DESCRIPTION This function is called by Update-T2TPostMigration function through the parameter -Destination .EXAMPLE PS C:\> Update-Target The following example run this function. #> # region import CSV $UserListToImportCheck = Get-CSVStatus -User if ( $UserListToImportCheck -eq 0 ) { Break } # region local variables if ( $FolderPath ) { $outFile = "$FolderPath\MigratedUsers.csv" } else { $outFile = "$home\desktop\MigratedUsers.csv" } $outArray = @() [System.Collections.ArrayList]$MEU = @() [System.Collections.ArrayList]$BreakLoop = @() $MEU = $ImportUserList.ExternalEmailAddressPostMove $BreakLoop = $ImportUserList.ExternalEmailAddressPostMove # Loop until all move requests from the MigratedUsers.csv # are Completed, CompletedWithWarning or Failed while ( $BreakLoop -ne $Null ) { foreach ( $i in $MEU ) { Write-Progress -Activity "Verifying move request status" -Status "Verifying user $($i)" # Reset variables $MoveRequest = $Null $object = New-Object System.Object # Get the move request. If it doesn't exist, set MoveRequestStatus as NotFound try { $MoveRequest = (Get-EXMoveRequest -Identity $i -ErrorAction Stop).Status } catch { Write-PSFMessage -Level Output -Message "MoveRequestNotExist: No move request for the user $($i) was found." $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $i $object | Add-Member -type NoteProperty -name MoveRequestStatus -value MoveRequestNotExist $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $Null $object | Add-Member -type NoteProperty -name Alias -value $Null $BreakLoop.Remove($i) } finally { if ( $MoveRequest -like "Completed*" ) { # We must resolve the Alias because ExternalEmailAddress isn't a valid # identity. We also need resolve the PrimarySMTPAddress cause it might # be used in Update-Source if UsePrimarySMTPAsTargetAddress is present. try { $user = Get-MailUser -identity $i -ErrorAction Stop if ( $? ) { $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $i $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $user.PrimarySMTPAddress $object | Add-Member -type NoteProperty -name Alias -value $user.Alias Enable-RemoteMailbox -identity $user.Alias -RemoteRoutingAddress $i -ErrorAction Stop | Out-Null if ( $? ) { Write-PSFMessage -Level Output -Message "Converted MailUser $($i) to RemoteMailbox and changed ExternalEmailAddress successfully." $object | Add-Member -type NoteProperty -name MoveRequestStatus -value Completed $BreakLoop.Remove($i) } } } catch { if ( Get-RemoteMailbox -Identity $user.Alias ) { Write-PSFMessage -Level Output -Message "AlreadyRemoteMailbox: User $($i) was not converted to RemoteMailbox because the object type is already RemoteMailbox." $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $i $object | Add-Member -type NoteProperty -name MoveRequestStatus -value IsAlreadyRemoteMailbox $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $Null $object | Add-Member -type NoteProperty -name Alias -value $Null $BreakLoop.Remove($i) } Else { Write-PSFMessage -Level Output -Message "MEUNotFound: The MailUser $($i) was not found." $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $i $object | Add-Member -type NoteProperty -name MoveRequestStatus -value MEUNotFound $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $Null $object | Add-Member -type NoteProperty -name Alias -value $Null $BreakLoop.Remove($i) } } } if ( $MoveRequest -like "Failed" ) { Write-PSFMessage -Level Output -Message "MoveRequestFailed: The $($i) move request was failed" $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $i $object | Add-Member -type NoteProperty -name MoveRequestStatus -value MoveRequestFailed $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $Null $object | Add-Member -type NoteProperty -name Alias -value $Null $BreakLoop.Remove($i) } } # add object to array only if there is MoveRequestStatus # to avoid empty lines while the move does not finish if ( $object.MoveRequestStatus ) { $outArray += $object } } # Remove from the $MEU array all objects that have MoveRequestStatus value # it means that the object should not go through the foreach anymore. foreach ($element in $outArray) { if ($element.MoveRequestStatus) { $MEU.Remove($element.ExternalEmailAddress) } } } # region export to a CSV Write-PSFMessage -Level Output -Message "Saving CSV on $($outfile)" $outArray | Export-CSV $outfile -notypeinformation # region clean up variables and sessions Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue Get-PSSession | Remove-PSSession Remove-Variable * -ErrorAction SilentlyContinue } 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 path which you mapped the source and target domains. You file header should have 2 columns and be: 'Source','Target' .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. .PARAMETER IncludeContacts Switch to get mail contacts. Mail contact dump also relies on the Custom Attibute filter. .PARAMETER IncludeSIP Switch to get SIP values from proxyAddresses. Without this switch the function returns only SMTP and X500. .PARAMETER IncludeManager Switch to get values from Manager attribute. Be sure to scope users and managers if this switch will be used. .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 -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: 2.0.0 Date: 2021.02.04 Authors: Denis Vilaca Signorelli (denis.signorelli@microsoft.com) Contributors: Agustin Gallegos (agustin.gallegos@microsoft.com) REQUIREMENT 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 provided AS IS without warranty of any kind and # # not supported under any Microsoft standard support program or service # ######################################################################### #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(DefaultParameterSetName="Default")] Param( [Parameter(Mandatory=$False, HelpMessage="Enter the user admin to connect to Exchange Online")] [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, HelpMessage="SwitchParameter to get mail contacts. Mail contact dump also relies on the Custom Attibute filter")] [switch]$IncludeContacts, [Parameter(Mandatory=$False, HelpMessage="SwitchParameter to get SIP values from proxyAddresses. Only SMTP and X500 are dumped by default")] [switch]$IncludeSIP, [Parameter(Mandatory=$false, HelpMessage="SwitchParameter to dump the manager AD attribute value")] [switch]$IncludeManager, [Parameter(Mandatory=$false, HelpMessage="SwitchParameter used to bypass the Auto-Expanding Archive check as it can increase the function duration")] [switch]$BypassAutoExpandingArchiveCheck, [Parameter(ParameterSetName="RemoteExchange",Mandatory=$false, HelpMessage="SwitchParameter to indicate that the machine running the function is not an Exchange Server")] [switch]$LocalMachineIsNotExchange, [Parameter(ParameterSetName="RemoteExchange",Mandatory=$true, HelpMessage="Enter the remote exchange hostname")] [string]$ExchangeHostname, [Parameter(Mandatory=$false, HelpMessage="Enter the file path used to save the function output. Default value is the Desktop path")] [string]$FolderPath ) 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)" # region local variables $outArray = @() if ( $FolderPath ) { $outFile = "$FolderPath\UserListToImport.csv" $AUXFile = "$FolderPath\AUXUsers.txt" } else { $outFile = "$home\desktop\UserListToImport.csv" $AUXFile = "$home\desktop\AUXUsers.txt" } # region global variables $Global:CustomAttribute = "CustomAttribute$CustomAttributeNumber" $Global:CustomAttributeValue = $Global:CustomAttributeValue $Global:DomainMappingCSV # Check if $DomainMappingCSV is valid $CSVMappingExist = Get-CSVStatus -MappingFile if ( $CSVMappingExist -eq 0 ) { Break } $Global:MappingCSV = Import-CSV -Path $DomainMappingCSV # region connections 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} } else { $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeLocal # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect } } # Dump contacts. TO DO: Run this function in parallel. Job is # not an option as this function relies on global variables if ( $IncludeContacts.IsPresent ) { Move-Contacts -Sync Export } # 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 $UsersCount = ($RemoteMailboxes | Measure-Object).count Foreach ($i in $RemoteMailboxes) { $counter++ Write-Progress -Activity "Exporting mailbox attributes to CSV" -Status "Working on $($i.DisplayName)" -PercentComplete ( $counter * 100 / $UsersCount ) $user = get-Recipient $i.alias $object = New-Object System.Object $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 RemoteRecipientType -value $i.RemoteRecipientType $object | Add-Member -type NoteProperty -name CustomAttribute -value $CustomAttribute $object | Add-Member -type NoteProperty -name CustomAttributeValue -value $CustomAttributeValue # We must resolve the manager's CN to alias if ( $IncludeManager.IsPresent -and $user.Manager -ne $Null ) { $Manager = ( Get-Recipient $user.Manager ).Alias $object | Add-Member -type NoteProperty -name Manager -value $Manager } if ( $IncludeManager.IsPresent -and $user.Manager -eq $Null ) { $object | Add-Member -type NoteProperty -name Manager -value $Null } 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 -Properties 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 -Properties 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-Expanding 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 ELC 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 only SMTP, X500 and SIP if the switch is present # from proxyAddresses and define the targetAddress $ProxyArray = @() $Proxy = $i.EmailAddresses foreach ($email in $Proxy) { if ($email -like 'SMTP:*' -or $email -like 'X500:*') { $ProxyArray = $ProxyArray += $email } if ($IncludeSIP.IsPresent -and $email -like 'SIP:*') { $ProxyArray = $ProxyArray += $email } if ($email -like 'SMTP:*' -and $email -like '*.onmicrosoft.com') { [string]$TargetString = $email -replace "smtp:","" } } # Join it using ";" and replace the old domain (source) to the new one (target) $ProxyToString = $ProxyArray -Join ";" -Replace "SMTP","smtp" $PrimarySMTP = $i.PrimarySmtpAddress # Map from the CSV which source domain will become which target 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 } if ( $PrimarySMTP -match $Domain.source ) { $PrimarySMTP = $PrimarySMTP -replace $SourceDomain,$TargetDomain } if ( $TargetString -match $Domain.source ) { $TargetPostMigration = $TargetString -replace $SourceDomain,$TargetDomain } } $object | Add-Member -type NoteProperty -name EmailAddresses -value $ProxyToString $object | Add-Member -type NoteProperty -name PrimarySMTPAddress -value $PrimarySMTP # Get ProxyAddress only for *.mail.onmicrosoft to define in the target AD the # targetAddress value. This value should not be converted through the domain mapping CSV. $object | Add-Member -type NoteProperty -name ExternalEmailAddress -value $TargetString # Get proxyAddress only for *.mail.onmicrosoft to define in the target AD the targetAddress # value post-migration. This value should be converted through the domain mapping CSV. $object | Add-Member -type NoteProperty -name ExternalEmailAddressPostMove -value $TargetPostMigration # Connect to AD exported module only if this machine has not AD Module installed if ( $LocalMachineIsNotExchange.IsPresent -and $LocalAD -eq '' ) { $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 } # region export the CSV if ( $AuxMessage ) { Write-PSFMessage -Level Output -Message "Saving CSV on $($outfile)" Write-PSFMessage -Level Output -Message "Saving TXT on $($AUXFile)" } else { Write-PSFMessage -Level Output -Message "Saving CSV on $($outfile)" } # region clean up variables and sessions $outArray | Export-CSV $outfile -notypeinformation Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue Get-PSSession | Remove-PSSession Remove-Variable * -ErrorAction SilentlyContinue } 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(-1 * $DaysOld) $csv = Import-Csv -Path $logFiles.FullName Switch ( $OutputType) { CSV { $csv | Select-Object @{N="Date";E={($_.timestamp -split " ")[0]}},@{N="Time";E={ Get-Date ($_.timestamp.Substring($_.timestamp.IndexOf(" ")).trim()) -Format HH:mm:ss }},` "ComputerName","Username","Level","Message","Type","FunctionName","ModuleName","File","Line","Tags","TargetObject","Runspace","Callstack" | Sort-Object Date -Descending | export-csv -Path $FilePath -NoTypeInformation } GridView { $csv | Select-Object @{N="Date";E={($_.timestamp -split " ")[0]}},@{N="Time";E={Get-Date ($_.timestamp.Substring($_.timestamp.IndexOf(" ")).trim()) -Format HH:mm:ss }},` "ComputerName","Username","Level","Message","Type","FunctionName","ModuleName","File","Line","Tags","TargetObject","Runspace","Callstack" | Sort-Object Date -Descending | 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 OUContacts Optional parameter if you want to create Mail Contacts 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 UserListToImport 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 ContactListToImport Optional parameter used to inform which path will be used import the CSV. If no path is chosen, the script will search for ContactListToImport.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: 2.0.0 Date: 2021.01.03 Author: Denis Vilaca Signorelli (denis.signorelli@microsoft.com) Contributors: Agustin Gallegos (agustin.gallegos@microsoft.com) REQUIREMENT 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 provided AS IS without warranty of any kind and # # not supported under any Microsoft standard support program or service # ######################################################################### #> [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=" SwitchParameter 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 the organization unit that mail objects will be created. The input is accepted as Name, Canonical name, Distinguished name (DN) or GUID")] [string]$OUContacts, [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]$UserListToImport, [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 ContactListToImport.csv")] [string]$ContactListToImport, [Parameter(ParameterSetName="RemoteExchange",Mandatory=$false, HelpMessage=" SwitchParameter to indicate that the machine running the function is not an Exchange Server")] [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)" # region local variables if ( $Password ) { $pwstr = $Password } else { $pwstr = "?r4mdon-_p@ss0rd!" } if ( $ResetPassword.IsPresent ) { [bool]$resetpwrd = $True } else { [bool]$resetpwrd = $False } $UPNSuffix = "@$UPNSuffix" $pw = New-Object "System.Security.SecureString"; # region global variables $Global:UserListToImport | Out-Null $Global:ContactListToImport | Out-Null $Global:ImportUserList | Out-Null $Global:ImportContactList | Out-Null $Global:OUContacts | Out-Null # region requirements check $UserListToImportCheck = Get-CSVStatus -User if ( $UserListToImportCheck -eq 0 ) { Break } $ContactListToImportCheck = Get-CSVStatus -Contact $AADCStopped = Get-Requirements -Requirements AADConnect if ( $AADCStopped -eq 0 ) { Break } #Region connections if ( $LocalMachineIsNotExchange ) { $Global:LocalMachineIsNotExchange = $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 $UsersCount = ( $ImportUserList | Measure-Object ).count foreach ( $user in $ImportUserList ) { $counter++ Write-Progress -Activity "Creating MEU objects and importing attributes from CSV" -Status "Working on $($user.DisplayName)" -PercentComplete ($counter * 100 / $UsersCount ) $tmpUser = $null $UPN = $user.Alias+$UPNSuffix # If OU was passed through param, honor it. # Otherwise create the MEU without OU specification if ( $OU ) { $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 $null -eq $LocalAD ) { 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 import 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 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 $null -eq $LocalAD ) { 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 $null -eq $LocalAD ) { 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 $null -eq $LocalAD ) { 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." } # Import Mail Contacts if ( $ContactListToImportCheck -eq 1 ) { Move-Contacts -Sync Import } # Import Manager value if the CSV contains the manager header $IncludeManager = $ImportUserList[0].psobject.Properties | Where { $_.Name -eq "Manager" } if ( $IncludeManager ) { Import-Manager -ObjType MEU } 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'" Get-PSSession | Remove-PSSession Remove-Variable * -ErrorAction SilentlyContinue } Function Update-T2TPostMigration { <# .SYNOPSIS Function developed to update objects once post-moverequest. .DESCRIPTION The Update-T2TPostMigration function is intended to update all necessary attributes in the source and target environment once the migration batch is finished. The function is divided between two switch parameters called -Source and -Destination. .PARAMETER Destination Required switch parameter to update the objects in the destination environment .PARAMETER AdminUPN Enter the user admin to connect to the target Exchange Online .PARAMETER UserListToImport Custom path to import the UserListToImport.csv. if no value is defined the function will try to get it from the Desktop. .PARAMETER MigratedUsersOutputPath Enter the file path used to save the MigratedUsers.csv. If no value is defined, default value will be the Desktop path .PARAMETER Source Required switch parameter to update the objects in the source environment. .PARAMETER MigratedUsers Custom path to import the MigratedUsers.csv. if no value is defined the function will try to get it from the Desktop. .PARAMETER UsePrimarySMTPAsTargetAddress Switch to indicate if the targetAddress (ExternalEmailAddress) will be the PrimarySMTPAddress. If not used, the default value is the MOERA domain .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:\> Update-T2TPostMigration -Destination -EXOAdmin admin@contoso.com 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:\> Update-T2TPostMigration -Source -EXOAdmin admin@contoso.com -UsePrimarySMTPAsTargetAddress 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: Update-T2TPostMigration.ps1 Version: 2.0.0 Date: 2021.04.21 Authors: Denis Vila�a Signorelli (denis.signorelli@microsoft.com) REQUIREMENT 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 provided AS IS without warranty of any kind and # # not supported under any Microsoft standard support program or service # ######################################################################### #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(DefaultParameterSetName="Default")] Param( [Parameter(ParameterSetName="Destination",Mandatory=$true, HelpMessage="Switch Parameter to indicate the destination tenant to connect")] [switch]$Destination, [Parameter(ParameterSetName="Destination",Mandatory=$true, HelpMessage="Enter the user admin to connect to Exchange Online")] [string]$AdminUPN, [Parameter(ParameterSetName="Destination",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]$UserListToImport, [Parameter(ParameterSetName="Destination",Mandatory=$false, HelpMessage="Enter the file path used to save the MigratedUsers.csv. If no value is defined, default value will be the Desktop path")] [string]$MigratedUsersOutputPath, [Parameter(ParameterSetName="Source",Mandatory=$true, HelpMessage="SwitchParameter to indicate that the machine running the function is not an Exchange Server")] [switch]$Source, [Parameter(ParameterSetName="Source",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 MigratedUsers.csv")] [string]$MigratedUsers, [Parameter(ParameterSetName="Source",Mandatory=$false, HelpMessage="Switch to indicate if the targetAddress (ExternalEmailAddress) will be the PrimarySMTPAddress. If not used, the default value is the MOERA domain")] [switch]$UsePrimarySMTPAsTargetAddress, [Parameter(Mandatory=$false, HelpMessage="SwitchParameter to indicate that the machine running the function is not an Exchange Server")] [switch]$LocalMachineIsNotExchange, [Parameter(Mandatory=$false, 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)" # region global variables if ( $MigratedUsers ) { $Global:MigratedUsers | Out-Null } if ( $MigratedUsersOutputPath ) { $Global:FolderPath | Out-Null} if ( $UsePrimarySMTPAsTargetAddress ) { $Global:UsePrimarySMTPAsTargetAddress | Out-Null } if ( $LocalMachineIsNotExchange ) { $Global:LocalMachineIsNotExchange | Out-Null } # region connections if ( $LocalMachineIsNotExchange.IsPresent -and $Destination.IsPresent ) { $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeRemote # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect -ExchangeHostname $ExchangeHostname } } elseif ( $Destination.IsPresent ) { $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeLocal # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect } } elseif ( $LocalMachineIsNotExchange.IsPresent -and $Source.IsPresent ) { $ServicesToConnect = Assert-ServiceConnection -Services ExchangeRemote # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect -ExchangeHostname $ExchangeHostname } } elseif ( $Source.IsPresent ) { $ServicesToConnect = Assert-ServiceConnection -Services ExchangeLocal # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect } } # region target function if ( $Destination.IsPresent ) { Update-Target } # region source function if ( $Source.IsPresent ) { Update-Source } } <# 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 |