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-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 Import-Manager { <# .SYNOPSIS Import Manager Attribute .DESCRIPTION Function called by Import-T2TAttributes if we found the Manager property on the UserListToImport.csv .PARAMETER CSVPath Path where the function can find the UserListToImport.csv .EXAMPLE PS C:\> Import-Manager Import the manager attribute valies from from the UserListToImport.csv #> Write-PSFMessage -Level Output -Message "Starting Manager attribute import" [int]$counter = 0 foreach ( $i in $ImportUserList ) { $counter++ Write-Progress -Activity "Importing Manager Attribute" -Status "Working on $($i.DisplayName)" -PercentComplete ($counter * 100 / $($ImportUserList.Count) ) 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" } } } } 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 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 -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.1.0 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(ParameterSetName="RemoteExchange",Mandatory=$false)] [switch]$LocalMachineIsNotExchange, [Parameter(ParameterSetName="RemoteExchange",Mandatory=$true, HelpMessage="Enter the remote exchange hostname")] [string]$ExchangeHostname, [Parameter(Mandatory=$False)] [switch]$IncludeSIP, [Parameter(Mandatory=$false)] [switch]$IncludeManager, [Parameter(Mandatory=$false)] [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)" if ( $FolderPath ) { $outFile = "$FolderPath\UserListToImport.csv" $AUXFile = "$FolderPath\AUXUsers.txt" } else { $outFile = "$home\desktop\UserListToImport.csv" $AUXFile = "$home\desktop\AUXUsers.txt" } $outArray = @() $CustomAttribute = "CustomAttribute$CustomAttributeNumber" $MappingCSV = Import-CSV -Path $DomainMappingCSV # Before move on getting the manager attribute # We need to know if we have the ADObjectId class if ($IncludeManager.IsPresent) { $ADObjectId = Get-TypeData -TypeName "Microsoft.Exchange.Data.ObjectId" } # 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} } else { $ServicesToConnect = Assert-ServiceConnection -Services EXO, ExchangeLocal # Connect to services if ServicesToConnect is not empty if ( $ServicesToConnect.Count ) { Connect-OnlineServices -AdminUPN $AdminUPN -Services $ServicesToConnect } } # 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 $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 we have don't have ADObjectId class, we must resolve the CN to alias if ( $IncludeManager.IsPresent -and $ADObjectId -eq $Null -and $user.Manager -ne $Null ) { $Manager = ( Get-Recipient $user.Manager ).Alias $object | Add-Member -type NoteProperty -name Manager -value $Manager } if ( $IncludeManager.IsPresent -and $ADObjectId -eq $Null -and $user.Manager -eq $Null ) { $object | Add-Member -type NoteProperty -name Manager -value $Null } # Under ADObjectId class (Exchange or Exchange management tools) the output is # array when getting manager property so just we need to declare the name element if ( $IncludeManager.IsPresent -and $ADObjectId -ne $Null -and $user.Manager -ne $Null ) { $object | Add-Member -type NoteProperty -name Manager -value $user.Manager.Name } if ( $IncludeManager.IsPresent -and $ADObjectId -ne $Null -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 = @() $TargetArray = @() $Proxy = $i.EmailAddresses foreach ($email in $Proxy) { if (($email -like 'SMTP:*' -or $email -like 'X500:*') -and $email -notlike '*.onmicrosoft.com') { $ProxyArray = $ProxyArray += $email } elseif ($IncludeSIP.IsPresent -and $email -like 'SIP:*') { $ProxyArray = $ProxyArray += $email } elseif ($email -like 'SMTP:*' -and $email -like '*.onmicrosoft.com') { $TargetArray = $TargetArray += $email } } # Join it using ";" and replace the old domain (source) to the new one (target) $ProxyToString = $ProxyArray -Join ";" -Replace "SMTP","smtp" # 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 # Get ProxyAddresses 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:","") # 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 } # Export to a CSV and clear up variables and sessions 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)" } $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(-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 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.1.0 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 ) { $pwstr = $Password } else { $pwstr = "?r4mdon-_p@ss0rd!" } if ( $FilePath ) { $global:ImportUserList = Import-CSV "$FilePath" } else { $global: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"; #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 } } #endregion 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 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 $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 Manager value if the CSV contains the manager header $IncludeManager = $ImportUserList[0].psobject.Properties | Where { $_.Name -eq "Manager" } if ( $IncludeManager ) { Import-Manager } 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 |