Exchange_AddIn.psm1


<#-----------------------------------------------------------------------------
PowerShell Module 'Exchange_AddIn'
 
Mike O'Neill, Microsoft Senior Premier Field Engineer (PFE)
https://mikeoneill.blog/
 
LEGAL DISCLAIMER
The sample scripts are not supported under any Microsoft standard support
program or service. The sample scripts are provided AS IS without warranty
of any kind. Microsoft further disclaims all implied warranties including,
without limitation, any implied warranties of merchantability or of fitness
for a particular purpose. The entire risk arising out of the use or
performance of the sample scripts 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 scripts or
documentation, even if Microsoft has been advised of the possibility of such
damages.
-----------------------------------------------------------------------------#>


<#
.SYNOPSIS
    Compilation of scripts and functions to help administrate Exchange on premises, Exchange online, Exchange hybrid, and Active Directory environments.
.DESCRIPTION
    This is a compilation of scripts and functions packaged into a single module with verb-noun cmdlets for engineers to easily run and rerun as needed.
.NOTES
    Current Version : 2.0
 
    History:
            2.0 - Updated xx/xx/xxxx
               - Added module prefix of 'EA' to all functions, for 'Exchange_AddIn'. This follows best practices function naming in custom modules.
               - Reverted back to Exchange_AddIn module for users to know what the module is used for. Too much unknown for what MO_Module was.
               - Updated all helpURI to Exchange_AddIn vs. MO_Module
               - Launched all websites for help topics in Exchange_AddIn module
 
            1.9.5 - Updated 5/8/2020
               - Fixed missing allowed Export-CSVAllADSites function in Functions to Export
 
            1.9.4 - Updated 3/31/2020
               - Added Set-EAEMailAddressesForUsers function
               - Starting to use 'EA' as a prefix for functions in this module.
 
            1.9.3 - Updated 3/17/2020
               - Added Export-CSVAllADSites function
               - allowed variable parameter in Set-AutoDiscoverSiteScopeExchangeServers
 
            1.9.2 - Updated 9/12/2019
               - Updated Get-DOTNETVersion function
 
            1.9.1 - Updated 9/12/2019
               - Set-AutoDiscoverSiteScopeExchangeServers validation testing
 
            1.9.0 - Updated 9/12/2019
               - Reverted back to Exchange_AddIn module for users to know what the module is used for. Too much unknown for what MO_Module was.
               - Beta testing before full 2.0 roll out. Changing all of the names to Exchange_AddIn module for help URI.
               - Testing internally with functions for larger organizations.
               - Added function: Get-ExchangeServerDBWhiteSpace
 
            1.2.5 - Updated 8/27/2019
               - Added additional helpuri values to several functions
               - Corrected example information on several functions
               - Modified Connect-ExchangeOnline to be Connect-ExchangeOnlineNonMFA. Exchange PG is about to release Connect-ExchangeOnline in new REST API module with MFA available.
 
            1.2.4 - Updated 7/29/2019
               - Removed mandatory=$false from connection functions
               - added Get-MailboxLocations as a function
               - Added helpuri to functions. https://mikeoneill.blog/MO_Module is online location with sub folders for each function
               - Added 3rd example for connection functions
               - Updated help topics grammar in functions for greater clarity
 
            1.2.3 - Updated 6/26/2019
               - Fixed confirm information when connecting to on premises Exchange Server
               - Ensured functions worked on Windows Server 2019
               - Ensured functions worked aganist Exchange Server 2019
               - Corrected grammar for functions
               - Confirmed functions of working status and troubleshooting of processes
 
            1.2.2 - Updated 6/25/2019
               - Added parameter help content to functions
               - Fixed many PS Script Analyzer problems that were identified
               - Added in functions
                  - Update-SpecificDLCSVImport
                  - Update-SpecificDLTXTImport
                  - Get-EXOEmailSecuritySettings
                  - Move-MailQueDatabase
               - Changed prefix values in logon functions to be just -prefix. Should help make the parameter value easier to type and understand when using those functions.
               - Updated Get-MailboxAutoReplyConfigurationDomain function including comment block with setting a value to test in organization.
               - Added confirmation display when connecting to an on premises Exchange Server
 
            1.2 - Updated 6/16/2019
               - Updates Changed title of module to a more generic name: MO_Module
               - Adding both Exchange and Active Directory functions into one module
               - Some functions are generic for operating systems
               - Add Write-Verbose to functions missing the feature
 
            1.1 - Posted 7/7/2017
               - Fixes:
                  - removed many 'Global' scopes in parameters in functions
                  - fixed duplicate parameter entries in functions
 
            1.0 - Posted 5/21/2017 - First iteration
 
 
.FUNCTIONALITY
   This module assists Exchange, Active Directory, and Azure engineers with a single point of reference with useful cmdlets in a PowerShell module.
#>


#region Connect to Exchange on-premises server

Function Request-CredentialExchangeOnPremises { # Request for new credentials function
   <#
   .SYNOPSIS
      Prompts user for a new user name and password for logon for onpremises server.
   .DESCRIPTION
      This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to a server on premises.
   .EXAMPLE
      Request-CredentialOnPremises
 
      This function was built to re-enter on premises credentials so as not to have to close the PS session to flush the variable.
   .INPUTS
       Requests users' credentials
   .FUNCTIONALITY
      Prompts for new on premises credentials.
   #>

   [cmdletbinding()]
   Param()
           &$Global:OnPremisesUserCredential
   } # End Request for new onpremises credentials function

   #region Enter Exchange onpremises Credentials to log onto onpremises server
        $Global:OnPremisesUserCredential = {
           $Global:OnPremisesCredential = Get-Credential -Message "Your Sign-In is your Domain\UserName."
   }

   #endregion Enter Credentials to log into on premises servers.

   Function Connect-ExchangeServer {
   <#
   .SYNOPSIS
      Connects to a designated Exchange in the computer parameter
   .DESCRIPTION
      Screen output of current status of DAG environment.
      Lists current Databases, what the status of each database is, and how many copies are available for each Database.
   .EXAMPLE
      Connect-ExchangeServer -Computer MBX01
 
      To connect up to an Exchange server on named MBX01 and not use cmdlet prefix option.
   .EXAMPLE
      Connect-ExchangeServer -Computer MBX04 -Prefix OnPrem
 
      This connects to Exchange server MBX04, and adds the prefix 'OnPrem' to all nouns in the remote PowerShell session.
   .EXAMPLE
      Connect-ExchangeServer -Computer EX12 -Prefix OP
 
      This connects to Exchange server EX12, and adds the prefix 'OP' to all nouns in the remote PowerShell session.
   .ROLE
      Exchange servers.
   .FUNCTIONALITY
      Connects remotely to an on premises Exchange server.
   #>

   [cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Connect-ExchangeServer')]
       param ([parameter(Mandatory=$true,
            HelpMessage="Enter a single computer name to create a remote PowerShell session.")]
            [string]$Computer,

            [parameter(HelpMessage="This is an optional parameter to add a prefix value to the Nouns in the remote PowerShell session to separate from other imported PowerShell sessions in the same PowerShell run space.")]
            [string]$Prefix = $Null
              )

   If ($OnPremisesCredential -eq $null) {
       &$Global:OnPremisesUserCredential
   }

   If ($OnPremisesPrefix -eq $Null) {
           $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential
           Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking
           Write-Verbose "`$Computer value is $Computer"
           $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer
           Write-Verbose ($ConnectedExchangeServer).name
            if ($ConnectedExchangeServer -ne $null) {
               Write-Host ""
               Write-Host "You are current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Green
               Write-Host ""
            }
            else {
               Write-Host ""
               Write-Host "You are NOT current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red
               Write-Host ""
            }
       }
   Else {
           $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential
           Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking -Prefix $OnPremisesPrefix
           Write-Verbose "`$OnPremisesComputer value is $Computer"
           Write-Verbose "`$Prefix value is $Prefix"
           $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer
           Write-Verbose ($ConnectedExchangeServer).name
            if ($ConnectedExchangeServer -ne $null) {
               Write-Host ""
               Write-Host "You are currently logged onto the Exchange server: $ConnectedExchangeServer, with remote PS prefix of: $Prefix." -ForegroundColor Green
               Write-Host ""
            }
            else {
               Write-Host ""
               Write-Host "You are NOT currently logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red
               Write-Host ""
            }
       }
   }
#endregion Connect to Exchange on-premises server

#region Exchange Online sign in

Function Request-CredentialExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Prompts user for a new user name and password for logon to tenant.
.DESCRIPTION
   This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to an O365 tenant.
.EXAMPLE
   Request-CredentialExchangeOnlineNonMFA
 
   This function was built to re-enter O365 credentials so as not to have to close the PS session to flush the variable.
.FUNCTIONALITY
   Prompts for new O365 credentials that log onto Exchange online.
#>

[cmdletbinding()]
param()
         &$Global:EXOUserCredential
} # End Request for new credentials function

#region Enter Exchange Online Credentials to log onto tenant
      $Global:EXOUserCredential = {
         $Global:EXOCredential = Get-Credential -Message "Your Sign-In is your e-mail address for O365."
}

#endregion Enter Credentials to log onto tenant

#region Exchange Online connect/disconnect session functions

Function Connect-ExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Creates a PSSession to connect to Exchange Online.
.DESCRIPTION
   This cmdlet combines the steps to successfully sign into an Exchange online tenant.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA
 
   To connect up to an Exchange online tenant and not use cmdlet prefix option.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA -prefix O365
 
   To connect up to an Exchange online tenant and use cmdlet prefix option, with 'O365' in this example.
.EXAMPLE
   Connect-ExchangeOnlineNonMFA -prefix EXO
 
   To connect up to an Exchange online tenant and use cmdlet prefix option, with 'EXO' in this example.
.FUNCTIONALITY
   Credentials are requested for an Exchange online tenant. A PSSession is then created using URL's to the Exchange online tenant, credentials are passed, remote module imported, and a confirmation that a connection is successful is displayed to the end user.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/connect-exchangeonlinenonmfa/')]
param ([parameter(HelpMessage="Optional prefix available to separate nouns for this PowerShell session from other imported PowerShell sessions in the same PowerShell run space.")]
      $Prefix = $Null)

If ($EXOCredential -eq $null)
{
      &$Global:EXOUserCredential
}
If ($Prefix -eq $Null) {

      $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection
      Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking
      Connect-MsolService -Credential $Global:EXOCredential

      $myMBX = Get-MailBox $EXOCredential.UserName

      Write-Host ""
      Write-Host "Hello $($myMBX), you are now logged into Exchange Online." -ForegroundColor Green
      Write-Host ""
      }
else {

      $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection
      Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking -Prefix $Prefix
      Connect-MsolService -Credential $Global:EXOCredential

      $myMBX = Get-MailBox $EXOCredential.UserName

      Write-Verbose "`$Prefix value is: $Prefix"

      Write-Host ""
      Write-Host "Hello $($myMBX), you are now logged into Exchange Online, using a noun prefix modifier of: $Prefix." -ForegroundColor Green
      Write-Host ""
      }
}

Function Disconnect-ExchangeOnlineNonMFA {
<#
.SYNOPSIS
   Disconnects a PSSession from Exchange Online.
.DESCRIPTION
   This cmdlet disconnects the sign-in session to an Exchange online tenant.
.EXAMPLE
   Disconnect-ExchangeOnlineNonMFA
 
   This function disconnects from the current Exchange online PowerShell session.
.FUNCTIONALITY
   Terminates the PSSession that is connected to an O365 Exchange online remote session and clears the credential variable.
#>


[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/disconnect-exchangeonlinenonemfa/')]
param()
      Remove-PSSession $EXOSession
      $Credential = $null
}

#endregion Exchange Online connect/disconnect session functions

#endregion Exchange Online sign in

#region Test database status and present information of DB's: Get-DAGDatabaseInformation

Function Get-DAGDatabaseInformation { #look into trap to stop the error of not being connected to an Exchange server
<#
.SYNOPSIS
   This cmdlet reports on the current status of databases.
.DESCRIPTION
   Screen output of current status of databases in the Exchange environment.
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
.PARAMETER
      $Command = "Get-DAGDatabaseAvailabilityGroup"
.PARAMETER
      $OldPreference = $ErrorActionPreference
.PARAMETER
      $ErrorActionPreference = ‘stop’
.EXAMPLE
   Get-DatabaseInformation
.ROLE
   Exchange servers.
.FUNCTIONALITY
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-dagdatabaseinformation/')]
   Param ($Command = "Get-DatabaseAvailabilityGroup")
      $OldPreference = $ErrorActionPreference
      $ErrorActionPreference = ‘stop’

try {
      if(Get-Command $command){

         if ($TestForDAG -eq $null) {
            "There is not a DAG in this Exchange environment."
         }
         Else {
            (Get-DatabaseAvailabilityGroup -Identity (Get-MailboxServer -Identity $Global:OnPremisesComputer).DatabaseAvailabilityGroup).Servers | Test-MapiConnectivity | Sort-Object Database | Format-Table -AutoSize
         }

         Get-MailboxDatabase | Sort-Object Name | Get-MailboxDatabaseCopyStatus | Format-Table -AutoSize
         function CopyCount {
            $DatabaseList = Get-MailboxDatabase | Sort-Object Name
            $DatabaseList | ForEach-Object {
                  $Results = $_ | Get-MailboxDatabaseCopyStatus
                  $Good = $Results | Where-Object { ($_.Status -eq "Mounted") -or ($_.Status -eq "Healthy") }
                  $_ | Add-Member NoteProperty "CopiesTotal" $Results.Count
                  $_ | Add-Member NoteProperty "CopiesFailed" ($Results.Count-$Good.Count)
            }
            $DatabaseList | Sort-Object copiesfailed -Descending | Format-Table name,copiesTotal,copiesFailed -AutoSize
         }
         CopyCount

         (Get-DatabaseAvailabilityGroup) | ForEach-Object {$_.Servers | ForEach-Object {Test-ReplicationHealth -Server $_}}
         }
      }

Catch {Write-Host "You need to connect to an Exchange Server first, before this function will process." -ForegroundColor Red}

Finally {$ErrorActionPreference=$oldPreference}

}

#endregion Test database status and present information of DB's: Get-DAGDatabaseInformation

#region Obtain .NET version currently installed on OS.

Function Get-DotNETVersion {
<#
.SYNOPSIS
   Displays .NET version on a computer.
.DESCRIPTION
   This cmdlet obtains the current version of .NET on either the local machine or a remote computer.
   Listed are the current version registry values and the related .NET version with the corresponding Exchange Server supported version.
.EXAMPLE
   Get-DotNETVersion
 
   Displays the current .NET version on the machine that this is run on.
.EXAMPLE
   Get-DotNETVersion -Computer ServerToTest
 
   This will display the .NET verion on the 'ServerToTest' in your environment.
.FUNCTIONALITY
   Displays the current release value and version of .NET.
#>


      [cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Get-DotNETVersion')]
      Param ([parameter(
         HelpMessage="Enter a computer name if needing to run this function against a remote machine.")]
         $Computer)

      If ($Computer -ne $null) {
      New-PSSession -ComputerName $Computer
}
         $NetValue = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name "Release" #Registry key value to test for version of .NET

<#Reference page of .NET values:
   Start-Process "https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx"
#>


      $NetValue | Select-Object Release, @{
         name=".NET Version / Supported Exchange Version(s)"
         expression={
      switch($_.Release) {
         378389 { " 4.5 / 2013 CU14" }
         378675 { " 4.5.1 / 2013 CU14, CU15" }
         378758 { " 4.5.1 / 2013 CU14, CU15" }
         379893 { " 4.5.2 / 2013 CU14, CU15 & 2016 CU3, CU4" }
         393295 { " 4.6 / None" }
         393297 { " 4.6 / None" }
         394254 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" }
         394271 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" }
         394802 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" }
         394806 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" }
         460798 { " 4.7 / None" }
         460805 { " 4.7 / None" }
         461308 { " 4.7.1 / 2013 CU19 and later, & 2016 CU9-CU12" }
         461310 { " 4.7.1 / 2013 CU19 and later, & 2016 CU9-CU12" }
         461808 { " 4.7.2 / 2013 CU21-CU25, & 2016 CU11-CU15, & 2019TRM-CU4" }
         461814 { " 4.7.2 / 2013 CU21-CU25, & 2016 CU11-CU15, & 2019TRM-CU4" }
         528040 { " 4.8 / 2013 CU22 and later, & 2016 CU13 and later, & 2019 CU2 and later" }
         528049 { " 4.8 / 2013 CU22 and later, & 2016 CU13 and later, & 2019 CU2 and later" }
         528209 { " 4.8 / 2013 CU22 and later, & 2016 CU13 and later, & 2019 CU2 and later" }
         default {"Unknown version of .NET and unknown support of Exchange Server"}
      }
      }
}

}

#endregion Obtain .NET version currently installed on OS.

#region Start DAG Maintenance Mode

Function Start-DAGMaintenanceMode {
<#
.SYNOPSIS
   Function to put an Exchange 2013/2016 Server into Maintenance Mode.
 
   Credits:
   --------
   Exchange Server 2013 Maintenance Mode Script (Start):
   https://gallery.technet.microsoft.com/exchange/Exchange-Server-2013-ff6c942f
 
   Checking for admin credentials:
   http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx
 
.DESCRIPTION
   This function is created to automatically put an Exchange 2010/2013/2016/2019 Server into Maintenance Mode.
   It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any.
 
.EXAMPLE
   Running the following command will place a server called "Server1" into Maintenance Mode.
 
   Start-ExchangeServerMaintenanceMode -Server Server1
.EXAMPLE
   Running the following command will place a server called "Server1" into Maintenance Mode and move any messages in transit from that server to "Server2".
   Please note that the TargetServer value has to be a FQDN!
 
   Start-ExchangeServerMaintenanceMode -Server Server1 -TargetServerFQDN Server2.domain.com
#>


[CmdletBinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/start-dagmaintenancemode/')]
Param
(
      # determine what server to put in maintenance mode
      [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
      [string]$Server,

      [Parameter(Mandatory=$false,
               ValueFromPipelineByPropertyName=$true,
               Position=1)]
      [string]$TargetServerFQDN = $Server
)

function evaluatequeues(){
      $MessageCount = Get-Queue -Server $Server | Where-Object {$_.Identity -notlike "*\Poison" -and $_.Identity -notlike"*\Shadow\*"} | Select-Object MessageCount
      $count = 0
      Foreach($message in $MessageCount){
         $count += $message.messageCount
      }
      if($count -ne 0){
         Write-Output "INFO: Sleeping for 30 seconds before checking the transport queues again..." -ForegroundColor Yellow
         Start-Sleep -s 30
         evaluatequeues
      }
      else{
         Write-Host "INFO: Transport queues are empty." -ForegroundColor Yellow
         Write-Host "INFO: Putting the entire server into maintenance mode..." -ForegroundColor Yellow
         if(Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance){
            Write-Host "INFO: Done! The components of $Server have successfully been placed into an inactive state!"
         }
         Write-Host "INFO: Restarting MSExchangeTransport service on server $Server..." -ForegroundColor Yellow
            #Restarting transport services based on info from http://blogs.technet.com/b/exchange/archive/2013/09/26/server-component-states-in-exchange-2013.aspx
            #Restarting the services will cause the transport services to immediately pick up the changed state rather than having to wait for a MA responder to take action
            Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport | Out-Null}

         #restart Front End Transport Services if server is also CAS
         if($discoveredServer.IsFrontendTransportServer -eq $true){
            Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow
            Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null
         }
         Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green
      }

}

$discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion

#Check for Administrative credentials
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
      Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!"
   Break
}

      if($discoveredServer.IsHubTransportServer -eq $True){
         if(-NOT ($TargetServerFQDN)){
            Write-Warning "TargetServerFQDN is required."
            $TargetServerFQDN = Read-Host -Prompt "Please enter the TargetServerFQDN: "
         }

         #Get the FQDN of the Target Server through DNS, even if the input is just a host name
         try{
            $TargetServer = ([System.Net.Dns]::GetHostByName($TargetServerFQDN)).Hostname
         }
         catch{
            Write-Warning "Could not resolve ServerFQDN: $TargetServerFQDN";break
         }

         if((Get-ExchangeServer -Identity $TargetServer | Select-Object IsHubTransportServer).IsHubTransportServer -ne $True){
            Write-Warning "The target server is not a valid Mailbox server."
            Write-Warning "Aborting script..."
            Break
         }

         #Redirecting messages to target system
         Write-Host "INFO: Suspending Transport Service. Draining remaining messages..." -ForegroundColor Yellow
         Set-ServerComponentState $Server -Component HubTransport -State Draining -Requester Maintenance
         Redirect-Message -Server $Server -Target $TargetServer -Confirm:$false

         #suspending cluster node (if the server is part of a DAG)
         $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup
         if($mailboxserver.DatabaseAvailabilityGroup -ne $null){
            Write-Host "INFO: Server $Server is a member of a Database Availability Group. Suspending the node now.`n" -ForegroundColor Yellow
            Write-Host "INFO: Node information:" -ForegroundColor Yellow
            Write-Host "-----------------------" -ForegroundColor Yellow
            Write-Host ""
            Write-Host ""
            Invoke-Command -ComputerName $Server -ArgumentList $Server {Suspend-ClusterNode $args[0]}
            Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $true
            Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Blocked
         }

         #Evaluate the Transport Queues and put into maintenance mode once all queues are empty
         evaluatequeues

      }
      else{
         Write-Host "INFO: Server $Server is a Client Access Server-only server." -ForegroundColor Yellow
         Write-Host "INFO: Putting the server components into inactive state" -ForegroundColor Yellow
         Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance
         Write-Host "INFO: Restarting transport services..." -ForegroundColor Yellow
         if(Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport | Out-Null}){
            Write-Host "INFO: Successfully restarted MSExchangeFrontEndTransport service" -ForegroundColor Yellow
         }
         Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green
      }
}

#endregion Start DAG Maintenance Mode

#region Stop DAG Maintenance Mode

Function Stop-DAGMaintenaceMode {
<#
.SYNOPSIS
   Function to take an Exchange 2013/2016 Server out of Maintenance Mode.
 
   Credits:
   --------
   Exchange Server 2013 Maintenance Mode Script (Stop):
   https://gallery.technet.microsoft.com/Exchange-Server-2013-77a71eb2
 
   Checking for admin credentials:
   http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx
 
.DESCRIPTION
   This function is created to take an Exchange 2010/2013/2016/2019 Server out of Maintenance Mode.
   It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any.
 
.EXAMPLE
   Running the following command will take a server called "Server1" out of Maintenance Mode:
 
   Stop-ExchangeServerMaintenanceMode -Server Server1
#>


[CmdletBinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/stop-dagmaintenancemode/')]
Param
(
      # determine what server to put in maintenance mode
      [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
      [string]$Server
)

$discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion

#Check for Administrative credentials
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
   Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!"
      Break
}

Write-Host "INFO: Reactivating all server components..." -ForegroundColor Yellow
      Set-ServerComponentState $server -Component ServerWideOffline -State Active -Requester Maintenance
Write-Host "INFO: Server component states changed back into active state using requester 'Maintenance'" -ForegroundColor Yellow

if($discoveredServer.IsHubTransportServer -eq $true){

      $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup

      if($mailboxserver.DatabaseAvailabilityGroup -ne $null){
         Write-Host "INFO: Server $server is a member of a Database Availability Group. Resuming the node now.`n" -ForegroundColor Yellow
         Write-Host "INFO: Node information:" -ForegroundColor Green
         Write-Host "-----------------------" -ForegroundColor Green
         Write-Host ""
         Invoke-Command -ComputerName $Server -ArgumentList $Server {Resume-ClusterNode $args[0]}
         Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $false
         Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Unrestricted
      }

      Write-Host "INFO: Resuming Transport Service..." -ForegroundColor Yellow
      Set-ServerComponentState –Identity $Server -Component HubTransport -State Active -Requester Maintenance

      Write-Host "INFO: Restarting the MSExchangeTransport Service on server $Server..." -ForegroundColor Yellow
      Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport} | Out-Null

}

#restart Front End Transport Services
if($discoveredServer.IsFrontendTransportServer -eq $true){
      Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow
      Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null
}

Write-Host ""
Write-Host "INFO: Done! Server $server successfully taken out of Maintenance Mode." -ForegroundColor Green
Write-Host ""

$ComponentStates = (Get-ServerComponentstate $Server).LocalStates | Where-Object {$_.State -eq "InActive"}
if($ComponentStates){
      Write-Warning "There are still some components inactive on server $Server."
      Write-Warning "Some features might not work until all components are back in an Active state."
      Write-Warning "Check the information below to see what components are still in an inactive state and which requester put them in that state."
      $ComponentStates
      Clear-Variable ComponentStates
}

}

#endregion Stop DAG Maintenance Mode

#region Update a Specific Distribution List

Function Update-SpecificDLCSVImport {
<#
.SYNOPSIS
   Function to update a specific DL.
.DESCRIPTION
   Simple function to import a CSV file with PrimarySMTPAddress as a column into a specific Distribution Group.
   The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members.
   https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx
.EXAMPLE
   Update-SpecificDLCSVImport -ImportPath c:\temp\file.csv -GroupToUpdate MyGroup
 
   This updates the 'MyGroup' DL with the content listed in c:\temp\file.csv file.
.EXAMPLE
   Update-SpecificDLCSVImport -ImportPath c:\users.csv -GroupToUpdate SecurityGroup
 
   This updates the 'SecurityGroup' DL with the content listed in c:\users.csv file.
.INPUTS
   Import Path of CSV file that contains PrimarySMTPAddress as a column.
.OUTPUTS
   Displays current list of users in requested group.
#>

[cmdletbinding(HelpUri="https://mikeoneill.blog/Exchange_AddIn/update-specificdlcsvimport/")]
param (
      [Parameter(Mandatory=$true,
      HelpMessage="Enter the import path of the file location, including the file name.")]
      $ImportPath,

      [Parameter(Mandatory=$true,
      HelpMessage="You must enter the name of the distribution group to update.")]
      $GroupToUpdate)

      # Import the members from the CSV
      $memberobjs = Import-Csv $ImportPath

      # Convert the member objects from the CSV into a property independent array
      # You can change PrimarySMTPAddress to be any property in the CSV that the Update-DistributionGroupmember cmdlet will take
      $memberobjs | ForEach-Object {[array]$newmembers = $newmembers + $_.PrimarySMTPAddress}

      # Update the DG membership
      Update-DistributionGroupmember -Identity $GroupToUpdate -members $newmembers -Confirm:$false

      # Output the new DG Membership
      Get-DistributionGroupMember -Identity $GroupToUpdate
}

Function Update-SpecificDLTXTImport {
<#
.SYNOPSIS
   Function to update a specific DL.
.DESCRIPTION
   Simple function to import a TXT file with PrimarySMTPAddress listings, no column header is needed.
   The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members.
   https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx
.EXAMPLE
   Update-SpecificDLTXTImport -ImportPath c:\temp\file.txt -GroupToUpdate MyGroup
 
   This updates the 'MyGroup' DL with the content listed in c:\temp\file.txt file.
.EXAMPLE
   Update-SpecificDLTXTImport -ImportPath c:\users.txt -GroupToUpdate SecurityGroup
 
   This updates the 'SecurityGroup' DL with the content listed in c:\users.txt file.
.INPUTS
   Import Path of TXT file that contains PrimarySMTPAddress as a list.
.OUTPUTS
   Displays current list of users in requested group.
#>

[cmdletbinding(HelpUri="https://mikeoneill.blog/Exchange_AddIn/update-specificdltxtimport/")]
param (
      [Parameter(Mandatory=$true,
      HelpMessage="Enter the import path of the file location, including the file name.")]
      $ImportPath,

      [Parameter(Mandatory=$true,
      HelpMessage="You must enter the name of the distribution group to update.")]
      $GroupToUpdate
   )

      # Create array of users via the Get-Content import cmdlet
      $newmembers = Get-Content $ImportPath

      # Update the DG membership
      Update-DistributionGroupmember -Identity $GroupToUpdate -Members $newmembers -Confirm:$false

      # Output the new DG Membership
      Get-DistributionGroupMember -Identity $GroupToUpdate
}

#endregion Update a Specific Distribution List

#region restart IIS on all Exchange Servers in organization

Function Restart-ExchangeIIS {
<#
.SYNOPSIS
   Restarts the IIS process on all Exchange servers in an Exchange organization.
.DESCRIPTION
   This function creates an array of all Exchange servers in an organization.
   Then it sends out an IIS restart command via PowerShell to all servers listed in the array.
.EXAMPLE
   Restart-ExchangeIIS
 
   Will request all Exchange servers with Get-ExchangeServer and restart IIS on the array built.
.ROLE
   Must already be logged onto an Exchange server PowerShell session for Get-ExchangeServer cmdlet to process.
#>


[cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Restart-ExchangeIIS')]
   param()
   $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell

   Write-Verbose "`$ExchangeServers array contains: $ExchangeServers"

ForEach ($ExchangeServer in $ExchangeServers) {
      Write-Host "Restarting the w3svc service on $($ExchangeServer.name)" -ForegroundColor Yellow
      Invoke-Command -ComputerName $ExchangeServer.name {Restart-Service w3svc -force}
      $Service = Get-Service -Name w3svc -ComputerName $ExchangeServer.name
      Write-Host "$($ExchangeServer.name) web service is $($Service.status)" -ForegroundColor Green
      Write-Host ""
   }
}

#endregion restart IIS on all Exchange Servers in organization

#region restart IIS on specific servers

Function Restart-IISOnServers {
<#
.SYNOPSIS
   Restart IIS on servers.
.DESCRIPTION
   Primarily designed to be used on Exchange servers, if IIS needs a restart, this function can be used on any servers running IIS.
   Load the parameter with a sinlge server or multiple servers wanting to process, then the invoke-command creates a remote connection to restart the IIS service.
.EXAMPLE
   Restart-IISOnServers -Computer PC1,PC2,PC3,PC4
 
   This will restart IIS on the following computers: PC1, PC2, PC3, PC4
.EXAMPLE
   Restart-IISOnServers -Computer DC1
 
   This will restart IIS on a single computer: DC1
.INPUTS
   Enter a single name or multiple names separated by commas to restart IIS on servers.
#>


[cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Restart-IISOnServers')]
param ([Parameter(Mandatory = $true,
   ValueFromPipeline=$false,
   ValueFromPipelineByPropertyName=$true,
   HelpMessage="Enter a single server or multiple server names, separated by commas, to restart IIS on the target computer(s).")]
   $Computer)

#Get-content -path c:\temp\listOfServersToBounce.txt = $ExchangeServers line to fix.
Write-Verbose "`$Computer array contains: $Computer"
      ForEach  ($Comp in $Computer)
      {
         Write-Host "Restarting the w3svc service on $Comp" -ForegroundColor Yellow
         Invoke-Command -ComputerName $Comp {Restart-Service w3svc -force}
         $Service = Get-Service -Name w3svc -ComputerName $Comp
         Write-Host "$Comp web service is $($Service.status)" -ForegroundColor Green
         Write-Host ""
      }

   $Computer = $null
}

#endregion restart IIS on specific servers

#region restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers

Function Restart-AutoDAppPool {
<#
.SYNOPSIS
   Restarts IIS AutoDiscover app pool on all Exchange servers in Organization.
.DESCRIPTION
   Obtains all Exchange servers in the organization and restarts the IIS AutoDiscover application pool on all servers.
.EXAMPLE
   Restart-AutoDAppPool
 
   Will request all Exchange servers with Get-ExchangeServer and restart AutoDiscover app pool on the array of servers built.
.NOTES
   This function allows repeatable use of restarting the IIS app pool for AutoDiscover on Exchange servers.
   This function will not restart the entire IIS service, just the specific AutoD app pool.
   Must be logged into an Exchange server remotely via PowerShell or Exchange Management Shell.
#>

   [cmdletbinding(helpuri='https://mikeoneill.blog/Exchange_AddIn/Restart-AutoDAppPool/',
                  SupportsShouldProcess=$true,ConfirmImpact='High')]
   Param()
      $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell

Write-Verbose "`$ExchangeServers array contains: $ExchangeServers"

      $ExchangeServers | ForEach-Object {
            if ($pscmdlet.ShouldProcess("$_", "Restarting AutoDiscover application pool"))
            {
               Write-Host "Recycling AutoDiscover application pool on $_" -ForegroundColor Yellow
               Invoke-Command -ComputerName $_.name -ScriptBlock { Get-ChildItem IIS:\AppPools | Where-Object { $_.name -like "*autodisc*" } | Restart-WebAppPool
            }
         }
      }
   $ExchangeServers = $null
}
#endregion restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers

#region Get: SPF, DMARC, DKIM from O365 tenant

Function Get-EXOEmailSecuritySettings {
<#
.SYNOPSIS
   Present the current Email Security Settings of an o365 tenant.
.DESCRIPTION
   Shows the: SPF, DKIM, and DMAC settings of a tenant.
   This function confirms the status of these settings in order to review if the different e-mail security settings have been enabled and/or are configured.
.EXAMPLE
   Get-EXOEmailSecuritySettings
 
   Gets the currently logged in tenant settings.
.EXAMPLE
   Get-EXOEmailSecuritySettings -AcceptedDomains contoso.com
 
   Gets the contoso.com in tenant settings.
.NOTES
   Creator: Matt Fields - PFE
   Updated: Mike O'Neill - PFE
 
   Version 1.0 - created code and functioned up for repeatable usage.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-exoemailsecuritysettings/')]
param($AcceptedDomains = $(Get-AcceptedDomain))

Write-Verbose "Accepted domains obtained: $($AcceptedDomains)"
ForEach ($Domain in $AcceptedDomains) {

#SPF Check
$SPF = Resolve-DnsName -Type TXT -Name $Domain -ErrorAction SilentlyContinue | Where-Object {$_.Strings -like "v=spf1*"}
Write-Verbose "Domain working on: $($domain)"
Write-Verbose "`$spf array contains: $spf"

If (!($SPF)) {
         Write-Host -ForegroundColor White -BackgroundColor Red "SPF has not been configured for $($Domain.DomainName)"
         Write-Host
      }

Else {
         $SPF | Format-Table -AutoSize
      }

#DKIM Check
$DKIM = Get-DKIMSigningConfig $Domain.DomainName
Write-Verbose "`$DKIM array contains: $DKIM"

IF ($DKIM.Enabled -eq 'True') {
      $DKIM = Resolve-DnsName -ErrorAction SilentlyContinue -Type CNAME -Name "selector1._domainkey.$($Domain.DomainName)"

      IF (!($DKIM)) {
            Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is enabled, but DNS entries have not been created for $($Domain.DomainName)"
            Write-Host
         }

      }
Else {
         Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is not enabled for $($Domain.DomainName)"
         Write-Host
      }

#DMARC Check
   $DMARC = Resolve-DnsName -ErrorAction SilentlyContinue -Type TXT -Name "_dmarc.$($Domain.DomainName)" | Where-Object {$_.Strings -like "v=DMARC1*"}
   Write-Verbose "`$DMARC array contains: $DMARC"

If (!($DMARC)) {
            Write-Host -ForegroundColor White -BackgroundColor Red "DMARC has not been configured for $($Domain.DomainName)"
            Write-Host
      }

Else {
         $DMARC | Format-Table -AutoSize
      }

      }
}

#endregion SPF, DMARC, DKIM from O365 tenant

#region Get domain specific OOF settings

Function Get-MailboxAutoReplyConfigurationDomain {
<#
.SYNOPSIS
   Lists OOF settings for users within the domain of specific SMTP addresses in a domain.
.DESCRIPTION
   Lists OOF settings if they are enabled and what values are set for users.
   This function only displays enabled OOF's and not disabled ones.
.EXAMPLE
   Get-MailboxAutoReployConfigurationDomain -Domain Contoso.com
 
   This lists end users with an @contoso.com SMTP address if they have OOF settings enabled.
.EXAMPLE
   Get-MailboxAutoReployConfigurationDomain -Domain contoso.onmicrosoft.com
 
   This lists end users with an @contoso.onmicrosoft.com SMTP address if they have OOF settings enabled.
.INPUTS
   Need to define the domain parameter.
.OUTPUTS
   Output to the screen of values that have OOF set.
 
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-mailboxautoreplyconfigurationdomain/')]
param([Parameter(Mandatory=$true,
   HelpMessage="Enter the domain value to be selected for scanning OOF values.")]
   $Domain)

Write-Verbose "`$Domain array contains: $Domain"

$Mailboxes=Get-Mailbox -ResultSize Unlimited | Where-Object {$_.PrimarySmtpAddress -like "*$Domain"}
Write-Verbose "`$Mailboxes array contains: $Mailboxes"

ForEach ($Mailbox in $Mailboxes)
   {
      $Mailbox | Get-MailboxAutoReplyConfiguration | Where-Object { $_.AutoReplyState -ne "Disabled" } | Select-Object Identity,StartTime,EndTime,AutoReplyState
   }

<#Demo to set for testing, change UserName to a valid value in the organization:
 
Scheudle OOF for testing of user.
Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState Scheduled -StartTime "$(Get-Date).adddays(1)" -EndTime "$(Get-Date).addmonths(1)" -InternalMessage "Internal auto-reply message"
 
Disable OOF testing for user.
Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState disabled
#>


}

#endregion Get domain specific OOF settings

#region Get membership count of distribution and security groups

Function Get-GroupMemberCount {
<#
.SYNOPSIS
   Get member count of groups.
.DESCRIPTION
   Gets all distribution groups and security enabled groups and presents name and member count of each group.
   Outputs the content into a csv file.
.EXAMPLE
   Get-GroupMemberCount
 
   This will obtain distribution group list in CSV export. Default path is: $env:temp\DistributionGroups.csv
.EXAMPLE
   Get-GroupMemberCount -path c:\temp -FileName DGs.csv
 
   This will obtain distribution group list in CSV exported file to c:\temp titled DGs.csv
.OUTPUTS
   CSV file titled: SecurityGroups.csv in path parameter, default "$env:temp" which is: 'c:\Users\<UserName>\AppData\Local\Temp'
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-GroupMemberCount/')]
param ($Path = $env:TEMP,
      $FileName = "DistributionGroups.csv")

$groups = @(Get-DistributionGroup -ResultSize unlimited) #Gets all distribution groups or e-mail enabled security groups.
Write-Verbose "`$groups array contains: $groups"

$report = @() #Creates empty array for report.

ForEach ($group in $groups)
      {
         $Count = @(Get-DistributionGroupMember -Identity $group.DistinguishedName).Count
         $ReportObject = New-Object PSObject
         $ReportObject | Add-Member NoteProperty -Name "GroupName" -Value $group.name
         $ReportObject | Add-Member NoteProperty -Name "Count" -value $Count
         $Report += $ReportObject
      }

$Report | Export-Csv -Path $Path\$FileName -NoTypeInformation

Write-Verbose "`$Groups array contains: $groups"
Write-Verbose "`$Path array contains: $Path"
Write-Verbose "`$FileName array contains: $FileName"

If (Test-Path -Path $Path\$FileName)
      {
      Write-Host "File $Path\$FileName exists." -ForegroundColor Green
      }

Else
      {
      Write-Host "File $myDir\$FileName was not created." -ForegroundColor Red
      }

}

#endregion Get membership count of distribution and security groups

#region Get Outlook bit value

Function Get-OutlookBitValue {
<#
.SYNOPSIS
   Get-OutlookBitValue
.DESCRIPTION
   Obtains Outlook bit version, 32 or 64, for a local Windows computer.
.EXAMPLE
   Get-OutlookBitValue
 
   Displays current version of Outlook if it is x86 (32bit) or x64 (64bit)
.OUTPUTS
   Outputs to screen
.NOTES
   Only currently works against a local machine. No remote parameter built in for function.
#>

[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-OutlookBitValue')]
Param()

"WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { (Get-ItemProperty -ea Ignore -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}}

"WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { $_.PSChildName -match "\d" } | Where-Object { Test-Path "$($_.PsPath)\Outlook" } | Where-Object { (Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}}

[string[]]$outlookRegKeys = 'HKLM:\SOFTWARE\Microsoft\Office\14.0\Outlook',
      'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\14.0\Outlook',
      'HKLM:\SOFTWARE\Microsoft\Office\15.0\Outlook',
      'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\15.0\Outlook',
      'HKLM:\SOFTWARE\Microsoft\Office\16.0\Outlook',
      'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\16.0\Outlook'

foreach ($outlookRegKey in $outlookRegKeys) {
      if (Test-Path $outlookRegKey) {
         $outlook = Get-ItemProperty -Path $outlookRegKey -Name 'Bitness'
         if ($outlook.Bitness -match '86') {
            Write-Host 'Outlook version is: x86 (32bit)' -ForegroundColor Green
         } else {
            Write-Host 'Outlook version is: x64 (64bit)' -ForegroundColor Green
         }
      }
}

}

#endregion Get Outlook bit value

#region Get-MailboxLocations

Function Get-MailboxLocations {
   <#
   .SYNOPSIS
      Displays locations of mailboxes in Exchange Online.
   .DESCRIPTION
      Determines the number of datacenters and locations where Exchange Online mailboxes are distributed.
   .EXAMPLE
      Get-MailboxLocations
 
      No parameters are needed.
   .OUTPUTS
      Displays content to screen.
   .NOTES
      Code used from Joe Palarchio from the Get-MailboxLocations.ps1 file. https://blogs.perficient.com/2016/03/15/office-365-script-to-determine-exchange-online-mailbox-location/
      Limitations: Table of datacenters is static and may need to be expanded as Microsoft brings additional datacenters online.
                        This function runs 'Get-Mailbox' which can take a really long time in EXO if the tenant has lots of mailboxes.
 
         This is not the 'Get-MailboxLocation' cmdlet: https://docs.microsoft.com/en-us/powershell/module/exchange/mailboxes/get-mailboxlocation?view=exchange-ps
   .COMPONENT
      Remote PowerShell connection to Exchange Online is required.
   #>


[cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-MailboxLocations')]
param()

      $Datacenter = @{}
      $Datacenter["CP"]=@("LAM","Brazil")
      $Datacenter["GR"]=@("LAM","Brazil")
      $Datacenter["HK"]=@("APC","Hong Kong")
      $Datacenter["SI"]=@("APC","Singapore")
      $Datacenter["SG"]=@("APC","Singapore")
      $Datacenter["KA"]=@("JPN","Japan")
      $Datacenter["OS"]=@("JPN","Japan")
      $Datacenter["TY"]=@("JPN","Japan")
      $Datacenter["AM"]=@("EUR","Amsterdam, Netherlands")
      $Datacenter["DB"]=@("EUR","Dublin, Ireland")
      $Datacenter["HE"]=@("EUR","Finland")
      $Datacenter["VI"]=@("EUR","Austria")
      $Datacenter["BL"]=@("NAM","Virginia, USA")
      $Datacenter["SN"]=@("NAM","San Antonio, Texas, USA")
      $Datacenter["BN"]=@("NAM","Virginia, USA")
      $Datacenter["DM"]=@("NAM","Des Moines, Iowa, USA")
      $Datacenter["BY"]=@("NAM","San Francisco, California, USA")
      $Datacenter["CY"]=@("NAM","Cheyenne, Wyoming, USA")
      $Datacenter["CO"]=@("NAM","Quincy, Washington, USA")
      $Datacenter["MW"]=@("NAM","Quincy, Washington, USA")
      $Datacenter["CH"]=@("NAM","Chicago, Illinois, USA")
      $Datacenter["ME"]=@("APC","Melbourne, Victoria, Australia")
      $Datacenter["SY"]=@("APC","Sydney, New South Wales, Australia")
      $Datacenter["KL"]=@("APC","Kuala Lumpur, Malaysia")
      $Datacenter["PS"]=@("APC","Busan, South Korea")
      $Datacenter["YQ"]=@("CAN","Quebec City, Canada")
      $Datacenter["YT"]=@("CAN","Toronto, Canada")

      Write-Host
      Write-Host "Gathering mailbox information..." -ForegroundColor Yellow

      $Mailboxes = Get-Mailbox -ResultSize Unlimited | Where-Object {$_.RecipientTypeDetails -ne "DiscoveryMailbox"}

      $ServerCount = ($Mailboxes | Group-Object {$_.ServerName}).count

      $Mailboxes = $Mailboxes | Group-Object {$_.ServerName.SubString(0,2)} | Select-Object @{Name="Datacenter";Expression={$_.Name}}, Count

      $Locations=@()

      # Not pretty error handling but allows counts to add properly when a datacenter location could not be identified from the table
      $E = $ErrorActionPreference
      $ErrorActionPreference = "SilentlyContinue"

      ForEach ($Mailbox in $Mailboxes) {
         $Object = New-Object -TypeName PSObject
         $Object | Add-Member -Name 'Datacenter' -MemberType NoteProperty -Value $Mailbox.Datacenter
         $Object | Add-Member -Name 'Region' -MemberType NoteProperty -Value $Datacenter[$Mailbox.Datacenter][0]
         $Object | Add-Member -Name 'Location' -MemberType NoteProperty -Value $Datacenter[$Mailbox.Datacenter][1]
         $Object | Add-Member -Name 'Count' -MemberType NoteProperty -Value $Mailbox.Count
         $Locations += $Object
      }

      $ErrorActionPreference = $E

      $TotalMailboxes = ($Locations | Measure-Object Count -Sum).sum

      $LocationsConsolidated = $Locations | Group-Object Location | ForEach-Object {
         New-Object PSObject -Property @{
         Location = $_.Name
         Mailboxes = ($_.Group | Measure-Object Count -Sum).Sum
         }
      } | Sort-Object Count -Descending

      Write-Host
      Write-Host -NoNewline "Your "
      Write-Host -NoNewline -ForegroundColor Yellow $TotalMailboxes
      Write-Host -NoNewline " mailboxes are spread across "
      Write-Host -NoNewline -ForegroundColor Yellow $ServerCount
      Write-Host -NoNewline " servers in "
      Write-Host -NoNewline -ForegroundColor Yellow $Locations.Count
      Write-Host -NoNewline " datacenters in "
      Write-Host -NoNewline -ForegroundColor Yellow $LocationsConsolidated.Count
      Write-Host " geographical locations."
      Write-Host
      Write-Host "The distribution of mailboxes is shown below:"

      $LocationsConsolidated | Select-Object Location, Mailboxes
      }

#endregion Get-MailboxLocations

#region Get-ExchangeServerDBWhiteSpace

Function Get-ExchangeServerDBWhiteSpace {
   <#
   .SYNOPSIS
      Displays white space on all Exchange databases in organization.
   .DESCRIPTION
      This function obtains current white space in all databases within an Exchange Server organization.
      This function could take a while to run the Get-MailboxDatabase -status for all Databases in organization.
      No parameters needed, it will just run against all Databases.
   .EXAMPLE
      Get-ExchangeDBWhiteSpace
 
      This will display all of the databases within the Exchange Server organization and their realative current white space.
   .OUTPUTS
      Displays content to screen.
   .ROLE
         Current PowerShell session needs to be used, either EMS on an Exchange server, or a remote PowerShell session connected to an Exchange server.
   #>


   [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-ExchangeServerDBWhiteSpace')]
   param ()

   Get-MailboxDatabase -Status | ForEach-Object {
      $DBName = $_.Name
      $whiteSpace = $_.AvailableNewMailboxSpace.ToMb()
      Write-Host "The $DBName database has $whiteSpace MB of total white space."
      }
   }

#endregion Get-ExchangeServerDBWhiteSpace

#region Set ADAutoD site

Function Export-CSVAllADSites {
   <#
   .SYNOPSIS
      Exports all AD sites within a domain.
   .DESCRIPTION
      Creates a CSV file to list out all of the sites and some of their properties within columns.
      This information can then be used for documentation or setting up an Exchange server site update for autodiscover site values.
   .EXAMPLE
      Function Export-CSVAllADSites
 
      This will export AD sites in the current forest to the default path of: $env:temp\ADSiteInfo.csv
   .EXAMPLE
      Function Export-CSVAllADSites -ReportFilePath c:\temp -ReportFileName ADSites.csv -CurrentForestName TailSpinToys.com
 
      This will export all AD sites within the TailSpinToys.com forest, to the c:\temp with the file name of: ADSites.csv
   .INPUTS
      Parameters:
   .OUTPUTS
      CSV file: $env:temp\ADSiteInfo.csv is the default.
   .NOTES
      General notes
   .ROLE
      Active Direoctory module needs to be available in the powershell session.
   #>

   [cmdletbinding()]
   param($ReportFilePath = "$env:temp",
         $ReportFileName = "ADSiteInfo.csv",
         $CurrentForestName = "Contoso.com"
        )

   $ReportFile = $ReportFilePath + '\' + $ReportFileName
   Write-Verbose "`$ReportFilePath value is $ReportFilePath"
   Write-Verbose "`$ReportFileName value is $ReportFileName"
   Write-Verbose "`$ReportFile value is $ReportFile"
   Write-Verbose "`$CurrentForestName value is $CurrentForestName"

   Remove-item $ReportFile -ErrorAction SilentlyContinue #removes file if already exists
   $ThisString="Identity,RegionForServers,ADSite,RegionForClients"
   Add-Content "$ReportFile" $ThisString

   $ADForest = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Forest", $CurrentForestName)
   [array]$Sites=[System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ADForest).sites
   $Sites #Displays sites to be collected.

   ForEach ($Site in $Sites)
       {
           $SiteName = $Site.Name

           $FinalVal=$SiteName
           Add-Content "$ReportFile" $FinalVal
       }


   If (Test-Path -Path $ReportFile)
       {
         Write-Host "File $ReportFile exists." -ForegroundColor Green
       }

   Else
       {
         Write-Host "File $ReportFile was not created." -ForegroundColor Red
       }

   }

Function Set-AutoDiscoverSiteScopeExchangeServers {
   <#
   .SYNOPSIS
      Sets Autodiscover AD Site scope on all Exchange servers in an organization.
   .DESCRIPTION
      This function allows repeatable tasks of Set-ClientAccessService for all Exchange servers the value of AutoDiscoverSiteScope to leverage an AD Deployment site.
      Additional information can be found at the Exchange team blog post:
      Start-Process https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Active-Directory-Deployment-Site/ba-p/604329
 
      The usage of an Exchange AD deployment site is recommended to ensure no Outlook disruptions when installing/adding another Exchange server to an organization.
      Without the proper AD site design setup, enviornments can have certifiate pop-ups appear within Outlook, which is not a desired result.
      This function also excludes any 'deploy' named AD sites in the assignment of the autodiscovery site scope.
 
      Additional information about the Autodiscover process:
      Start-Process 'https://docs.microsoft.com/en-us/previous-versions/office/exchange-server-2007-technical-articles/bb332063(v=exchg.80)'
   .EXAMPLE
      Set-AutoDiscoverSiteScopeExchangeServers
 
      Set the Autodiscover site value of all Exchange servers in an AD Forest with excluding the default exlusion sites that match the search of *deploy*.
   .EXAMPLE
      Set-AutoDiscoverSiteScopeExchangeServers -DeployADSiteExlusion "*test*"
 
      Set the Autodiscover site value of all Exchange servers in an AD Forest and will exlude setting any searched names of *test* in the AD forest to the AutodiscoverSiteScope on Exchnage servers.
   .EXAMPLE
      Set-AutoDiscoverSiteScopeExchangeServers -ImportFile c:\temp\ADSite.csv -DeployADSiteExlusion "*test*"
 
      Set the Autodiscover site value of all Exchange servers in an AD Forest to the values listed in the CSV file, specifically based on their related regions and will exlude setting any searched names of *test* in the AD forest to the AutodiscoverSiteScope on the targetted Exchnage servers.
   .NOTES
       The default scope is all Exchnage servers in the entire organiztion.
       Option to import selected CSV file, to scope both region and servers to be targetted for update.
   .OUTPUTS
      Screen data.
   .COMPONENT
      Needs to be connected to an Exchange server remotely via PowerShell or using Exchange Management Shell.
   #>


   [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='High',HelpUri='https://mikeoneill.blog/Exchange_AddIn/Set-AutoDSitesAllExchangeServers/')]
   param ([String]$DeployADSiteExclusion ="*deploy*", #This allows AD sites with deploy in the name anywhere, to be excluded from being assigned to Exchange Servers. Single value entry allowed.
   $ImportFile = $null #CSV file to be used if assigning regions or scoping AD sites per data centers.
   )

   Write-Host "Values currently set on Exchange servers:" -ForegroundColor Yellow
   Get-ClientAccessService | Format-Table Name,AutoDiscoverSiteScope –Autosize

   if ([string]::IsNullOrWhiteSpace($ImportFile)){
      $Servers = Get-ExchangeServer
      $AutoDSiteScope = (Get-ADSite | Where-Object {$_.name -notlike "$DeployADSiteExclusion"}).name

         Write-Verbose "`$Servers value is $Servers"
         Write-Verbose "`$AutoDSiteScope value is $AutoDSiteScope"
         Write-Verbose "`$DeployADSiteExclusion value is $DeployADSiteExclusion"

         Write-Host "The domain contains the following AD (excluding any $DeployADSiteExclusion) sites:" -ForegroundColor Green
         $AutoDSiteScope
         Write-Host ""

      ForEach ($Server in $Servers)
      {
          If ($pscmdlet.ShouldProcess($Server.Identity, "Setting AutoDiscover site scope"))
              {
                  Set-ClientAccessServer $Server.Identity –AutoDiscoverSiteScope $AutoDSiteScope -WarningAction SilentlyContinue
              }
      }
   }
   else {
      $servers = Import-Csv $ImportFile | Select-Object RegionForServers, Identity | Where-Object {$_.servers -ne ""}
      $exchange = New-Object 'System.Collections.Generic.Dictionary[string,string[]]'
      $items = Import-Csv $ImportFile | Select-Object RegionForClients, ADsite

      foreach($item in $items) {
          if(-not ($exchange.ContainsKey($item.RegionForClients))) {
              $exchange.Add($item.RegionForClients, $item.adsite)
          }
          else {
              $exchange[$item.RegionForClients] += $item.adsite
          }
      }

      foreach($AutoDiscoverySiteScope in $exchange.Keys) {
          $keyValues = $exchange[$AutoDiscoverSiteScope] -join ","

          if(($servers | Where-Object RegionForServers -eq $AutoDiscoverSiteScope | measure-object).count -gt 0) {
          Write-Host ("Invoke commands for RegionForServer {0}" -f $AutoDiscoverSiteScope)

            foreach($server in $servers | Where-Object RegionForServers -eq $AutoDiscoverSiteScope) {

               Write-Host ("Writing -Identity {0} -AutoDiscoverSiteScope {1}" -f $server.Identity, $keyValues)

               If ($pscmdlet.ShouldProcess($Server.Identity, "Setting AutoDiscover site scope"))
                     {
                        Set-ClientAccessServer -Identity $Server.Identity –AutoDiscoverSiteScope $keyValues -WarningAction SilentlyContinue
                     }
            }
          }
      }
   }

   Write-Host "Values now currently set on Exchange servers:" -ForegroundColor Green
   Get-ClientAccessServer -WarningAction SilentlyContinue | Format-Table Name,AutoDiscoverSiteScope –Autosize -Wrap

} #end of function

#endregion Set ADAutoD site

#region Function Set-EAEMailAddressesForUsers

Function Set-EAEMailAddressesForUsers {
   <#
   .SYNOPSIS
      Sets E-Mail addresses for a defined list of users, using a set list of values.
   .DESCRIPTION
      This fuction uses a CSV import file to add an SMTP address to users and sets the new one as the primary.
      This allows a defined scope of users to only add the new value as needed.
      The CSV file must have two (2) columns: alias,addnewemailaddress
      Bu defining the alias and the needed new email address, it allows a specific value and set of users to only need to be updated.
   .EXAMPLE
      Set-EAEMailAddressesForUsers -InputFile c:\temp\AddSmtpMakePrimaryProxy.csv
 
      Imports the file of c:\temp\AddSmtpMakePrimaryProxy.csv, and then sets the values listed in file.
   .EXAMPLE
      Set-EAEMailAddressesForUsers -InputFile c:\names\AddSmtpPrimary.csv -verbose
 
      Imports the file of c:\names\AddSmtpPrimary.csv, shows verbose content, and then sets the values listed in file.
   .EXAMPLE
      Set-EAEMailAddressesForUsers -InputFile c:\temp\AddSmtpMakePrimaryProxy.csv -WhatIf
 
      Imports the file of c:\temp\AddSmtpMakePrimaryProxy.csv and only runs in WhatIf mode with not setting any values to any aliases.
   .INPUTS
      CSV file is required. Needs 2 columns: alias and the new email address requested.
      Ensure the CSV file is NOT opened when trying to import into this function to run updates for users.
   .OUTPUTS
      Screen output of updated aliases
   .NOTES
      Built March 28, 2020 for specific add of e-mail new address and set as primary.
      Additional options may be built in later if needed.
   .FUNCTIONALITY
      This function is only designed to accept one new e-mail address currently, add it to the related alias, and set that new value as the default reply address.
   #>


   [cmdletbinding(SupportsShouldProcess=$true,
                   HelpUri = 'https://mikeoneill.blog/exchange_addin/set-eaemailaddressesforusers/',
                   ConfirmImpact='High')]

   param (
       [parameter(HelpMessage="Enter computer name to run function againist.")]
       $InputFile = "c:\temp\AddSmtpMakePrimaryProxy.csv",
       [ValidateSet("MailContact", "MailNonUniversalGroup", "MailUniversalDistributionGroup","MailUniversalSecurityGroup","MailUser","PublicFolder","UserMailbox")]
         $MailObjectType)

   Begin {
           Write-Host "Starting the set process..." -ForegroundColor Yellow
       }

   Process {
       Write-Verbose "`InputFile array contains the value $InputFile"

   $InputFile | ForEach-Object {

       $userName = Import-Csv $InputFile | Select-Object alias
       Write-Verbose "`$UserName array contains $UserName"

       $user = Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType
       Write-Verbose "`$User array contains $User"

       $AddNewemailAddress = Import-csv $InputFile | Select-Object AddNewEmailAddress
       Write-Verbose "`$AddNewemailAddress array contains: $AddNewemailAddress"

       $useremailaddresses = Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType | Select-Object -ExpandProperty emailaddresses
       $useremailaddresses = $useremailaddresses -creplace 'SMTP', 'smtp'
       Write-Verbose "`$useremailaddresses array contains $useremailaddresses"
       $useremailaddresses += "SMTP:$($AddNewemailAddress.addnewemailaddress)"

       if ($pscmdlet.ShouldProcess("$user", "Setting $useremailaddresses"))
           {
               Update-Recipient -Identity $userName.alias -RecipientType $MailObjectType -emailAddresses $useremailaddresses
           }

       Write-Host "The user: $($user.alias) has the following addresses set;" -ForegroundColor Green
       Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType | Format-List EmailAddresses, PrimarySmtpAddress
       }

       }

       end {
           Write-Host "Task complete for all current users in CSV file." -ForegroundColor Green
       }
   }

   Set-EAEMailAddressesForUsers -InputFile c:\temp\AddSmtpMakePrimaryProxy.csv -verbose -MailObjectType MailContact -WhatIf

#endregion Function Set-EAEMailAddressesForUsers


#region Viewing User values of direct assigned O365 licensing and plans within O365/M365
<# This process and functions are from:
start-process https://agbo.blog/2019/08/22/how-is-your-office-365-users-licenses-assigned-direct-or-inherited/
 
Author: Jean-Marie AGBO
#>


function Get-LicensePlan {
<#
.Synopsis
   Helper function for Get-LAPATH function. Get-LicensePlan information from tenant.
.DESCRIPTION
   Get-LicensePlan information from tenant obtains array listing of O365/M365 plans.
.INPUTS
   None
.OUTPUTS
   None
.NOTES
   start-process https://agbo.blog/2019/08/22/how-is-your-office-365-users-licenses-assigned-direct-or-inherited/
   Author: Jean-Marie AGBO
   Updated: Mike O'Neill
.COMPONENT
   Need to run Connect-MSOLService in order to get Azure AD information from the MS Online realm.
#>

   param (

       [Parameter(Mandatory=$true)]
       [String]$SkuId,
       [Parameter(mandatory=$true)]
       [String]$TenantName

   )

<# For current listing of all SKU's
Start-Process https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference
#>

   Switch($SkuId){

      "$($TenantName):SPZA_IW" {return "APP CONNECT IW"}
      "$($TenantName):MCOMEETADV" {return "Microsoft 365 Audio Conferencing"}
      "$($TenantName):AAD_BASIC" {return "AZURE ACTIVE DIRECTORY BASIC"}
      "$($TenantName):AAD_PREMIUM" {return "AAD Premium P1"}
      "$($TenantName):AAD_PREMIUM_P2" {return "AAD Premium P2"}
      "$($TenantName):RIGHTSMANAGEMENT" {return "AZURE INFORMATION PROTECTION PLAN 1"}
      "$($TenantName):DYN365_ENTERPRISE_PLAN1" {return "DYNAMICS 365 CUSTOMER ENGAGEMENT PLAN ENTERPRISE EDITION"}
      "$($TenantName):DYN365_ENTERPRISE_CUSTOMER_SERVICE" {return "DYNAMICS 365 FOR CUSTOMER SERVICE ENTERPRISE EDITION"}
      "$($TenantName):DYN365_FINANCIALS_BUSINESS_SKU" {return "DYNAMICS 365 FOR FINANCIALS BUSINESS EDITION"}
      "$($TenantName):DYN365_ENTERPRISE_SALES_CUSTOMERSERVICE" {return "DYNAMICS 365 FOR SALES AND CUSTOMER SERVICE ENTERPRISE EDITION"}
      "$($TenantName):DYN365_ENTERPRISE_SALES" {return "DYNAMICS 365 FOR SALES ENTERPRISE EDITION"}
      "$($TenantName):DYN365_ENTERPRISE_TEAM_MEMBERS" {return "EDITION"}
      "$($TenantName):Dynamics_365_for_Operations" {return "DYNAMICS 365 UNF OPS PLAN ENT EDITION"}
      "$($TenantName):EMS" {return "ENTERPRISE MOBILITY + SECURITY E3"}
      "$($TenantName):EMSPREMIUM" {return "ENTERPRISE MOBILITY + SECURITY E5"}
      "$($TenantName):EXCHANGESTANDARD" {return "EXCHANGE ONLINE (PLAN 1)"}
      "$($TenantName):EXCHANGEENTERPRISE" {return "EXCHANGE ONLINE (PLAN 2)"}
      "$($TenantName):EXCHANGEARCHIVE_ADDON" {return "EXCHANGE ONLINE ARCHIVING FOR EXCHANGE ONLINE"}
      "$($TenantName):EXCHANGE ONLINE ARCHIVING FOR EXCHANGE SERVER" {return "EXCHANGEARCHIVE"}
      "$($TenantName):EXCHANGEESSENTIALS" {return "EXCHANGE ONLINE ESSENTIALS"}
      "$($TenantName):EXCHANGE_S_ESSENTIALS" {return "EXCHANGE ONLINE ESSENTIALS"}
      "$($TenantName):EXCHANGEDESKLESS" {return "EXCHANGE ONLINE KIOSK"}
      "$($TenantName):EXCHANGETELCO" {return "EXCHANGE ONLINE POP"}
      "$($TenantName):INTUNE_A" {return "INTUNE"}
      "$($TenantName):INTUNE_A_VL" {return "INTUNE"}
      "$($TenantName):M365EDU_A1" {return "Microsoft 365 A1"}
      "$($TenantName):M365EDU_A3_FACULTY" {return "Microsoft 365 A3 for faculty"}
      "$($TenantName):M365EDU_A3_STUDENT" {return "Microsoft 365 A3 for students"}
      "$($TenantName):M365EDU_A5_FACULTY" {return "Microsoft 365 A5 for faculty"}
      "$($TenantName):M365EDU_A5_STUDENT" {return "Microsoft 365 A5 for students"}
      "$($TenantName):O365_BUSINESS" {return "MICROSOFT 365 APPS FOR BUSINESS"}
      "$($TenantName):SMB_BUSINESS" {return "MICROSOFT 365 APPS FOR BUSINESS"}
      "$($TenantName):OFFICESUBSCRIPTION" {return "MICROSOFT 365 APPS FOR ENTERPRISE"}
      "$($TenantName):O365_BUSINESS_ESSENTIALS" {return "MICROSOFT 365 BUSINESS BASIC"}
      "$($TenantName):SMB_BUSINESS_ESSENTIALS" {return "MICROSOFT 365 BUSINESS BASIC"}
      "$($TenantName):O365_BUSINESS_PREMIUM" {return "MICROSOFT 365 BUSINESS STANDARD"}
      "$($TenantName):SMB_BUSINESS_PREMIUM" {return "MICROSOFT 365 BUSINESS STANDARD"}
      "$($TenantName):SPB" {return "MICROSOFT 365 BUSINESS PREMIUM"}
      "$($TenantName):SPE_E3" {return "MICROSOFT 365 E3"}
      "$($TenantName):SPE_E5" {return "Microsoft 365 E5"}
      "$($TenantName):SPE_E3_USGOV_DOD" {return "Microsoft 365 E3_USGOV_DOD"}
      "$($TenantName):SPE_E3_USGOV_GCCHIGH" {return "Microsoft 365 E3_USGOV_GCCHIGH"}
      "$($TenantName):INFORMATION_PROTECTION_COMPLIANCE" {return "Microsoft 365 E5 Compliance"}
      "$($TenantName):IDENTITY_THREAT_PROTECTION" {return "Microsoft 365 E5 Security"}
      "$($TenantName):IDENTITY_THREAT_PROTECTION_FOR_EMS_E5" {return "Microsoft 365 E5 Security for EMS E5"}
      "$($TenantName):M365_F1" {return "Microsoft 365 F1"}
      "$($TenantName):DESKLESSPACK" {return "OFFICE 365 F1"}
      "$($TenantName):DESKLESSPACK" {return "OFFICE 365 F3"}
      "$($TenantName):SPE_F1" {return "Microsoft 365 F3"}
      "$($TenantName):FLOW_FREE" {return "FLOW FREE"}
      "$($TenantName):MCOEV" {return "MICROSOFT 365 PHONE SYSTEM"}
      "$($TenantName):MCOEV_DOD" {return "MICROSOFT 365 PHONE SYSTEM FOR DOD"}
      "$($TenantName):MCOEV_FACULTY" {return "MICROSOFT 365 PHONE SYSTEM FOR FACULTY"}
      "$($TenantName):MCOEV_GOV" {return "MICROSOFT 365 PHONE SYSTEM FOR GCC"}
      "$($TenantName):MCOEV_GCCHIGH" {return "MICROSOFT 365 PHONE SYSTEM FOR GCCHIGH"}
      "$($TenantName):MCOEVSMB_1" {return "MICROSOFT 365 PHONE SYSTEM FOR SMALL AND MEDIUM BUSINESS"}
      "$($TenantName):MCOEV_STUDENT" {return "MICROSOFT 365 PHONE SYSTEM FOR STUDENTS"}
      "$($TenantName):MCOEV_TELSTRA" {return "MICROSOFT 365 PHONE SYSTEM FOR TELSTRA"}
      "$($TenantName):MCOEV_USGOV_DOD" {return "MICROSOFT 365 PHONE SYSTEM_USGOV_DOD"}
      "$($TenantName):MCOEV_USGOV_GCCHIGH" {return "MICROSOFT 365 PHONE SYSTEM_USGOV_GCCHIGH"}
      "$($TenantName):WIN_DEF_ATP" {return "Microsoft Defender Advanced Threat Protection"}
      "$($TenantName):CRMPLAN2" {return "MICROSOFT DYNAMICS CRM ONLINE BASIC"}
      "$($TenantName):CRMSTANDARD" {return "MICROSOFT DYNAMICS CRM ONLINE"}
      "$($TenantName):IT_ACADEMY_AD" {return "MS IMAGINE ACADEMY"}
      "$($TenantName):TEAMS_FREE" {return "MICROSOFT TEAM (FREE)"}
      "$($TenantName):ENTERPRISEPREMIUM_FACULTY" {return "Office 365 A5 for faculty"}
      "$($TenantName):ENTERPRISEPREMIUM_STUDENT" {return "Office 365 A5 for students"}
      "$($TenantName):EQUIVIO_ANALYTICS" {return "Office 365 Advanced Compliance"}
      "$($TenantName):ATP_ENTERPRISE" {return "Office 365 Advanced Threat Protection (Plan 1)"}
      "$($TenantName):STANDARDPACK" {return "E1"}
      "$($TenantName):STANDARDWOFFPACK" {return "OFFICE 365 E2"}
      "$($TenantName):ENTERPRISEPACK" {return "E3"}
      "$($TenantName):DEVELOPERPACK" {return "OFFICE 365 E3 DEVELOPER"}
      "$($TenantName):ENTERPRISEPACK_USGOV_DOD" {return "Office 365 E3_USGOV_DOD"}
      "$($TenantName):ENTERPRISEPACK_USGOV_GCCHIGH" {return "Office 365 E3_USGOV_GCCHIGH"}
      "$($TenantName):ENTERPRISEWITHSCAL" {return "OFFICE 365 E4"}
      "$($TenantName):ENTERPRISEPREMIUM" {return "E5"}
      "$($TenantName):ENTERPRISEPREMIUM_NOPSTNCONF" {return "OFFICE 365 E5 WITHOUT AUDIO CONFERENCING"}
      "$($TenantName):MIDSIZEPACK" {return "OFFICE 365 MIDSIZE BUSINESS"}
      "$($TenantName):LITEPACK" {return "OFFICE 365 SMALL BUSINESS"}
      "$($TenantName):LITEPACK_P2" {return "OFFICE 365 SMALL BUSINESS PREMIUM"}
      "$($TenantName):WACONEDRIVESTANDARD" {return "ONEDRIVE FOR BUSINESS (PLAN 1)"}
      "$($TenantName):WACONEDRIVEENTERPRISE" {return "ONEDRIVE FOR BUSINESS (PLAN 2)"}
      "$($TenantName):POWERAPPS_PER_USER" {return "POWER APPS PER USER PLAN"}
      "$($TenantName):POWER_BI_STANDARD" {return "POWER BI (FREE)"}
      "$($TenantName):POWER_BI_ADDON" {return "POWER BI FOR OFFICE 365 ADD-ON"}
      "$($TenantName):POWER_BI_PRO" {return "POWER BI PRO"}
      "$($TenantName):PROJECTCLIENT" {return "PROJECT FOR OFFICE 365"}
      "$($TenantName):PROJECTESSENTIALS" {return "PROJECT ONLINE ESSENTIALS"}
      "$($TenantName):PROJECTPREMIUM" {return "PROJECT ONLINE PREMIUM"}
      "$($TenantName):PROJECTONLINE_PLAN_1" {return "PROJECT ONLINE PREMIUM WITHOUT PROJECT CLIENT"}
      "$($TenantName):PROJECTPROFESSIONAL" {return "PROJECT ONLINE PROFESSIONAL"}
      "$($TenantName):PROJECTONLINE_PLAN_2" {return "PROJECT ONLINE WITH PROJECT FOR OFFICE 365"}
      "$($TenantName):SHAREPOINTSTANDARD" {return "SHAREPOINT ONLINE (PLAN 1)"}
      "$($TenantName):SHAREPOINTENTERPRISE" {return "SHAREPOINT ONLINE (PLAN 2)"}
      "$($TenantName):MCOIMP" {return "SKYPE FOR BUSINESS ONLINE (PLAN 1)"}
      "$($TenantName):MCOSTANDARD" {return "SKYPE FOR BUSINESS ONLINE (PLAN 2)"}
      "$($TenantName):MCOPSTN2" {return "SKYPE FOR BUSINESS PSTN DOMESTIC AND INTERNATIONAL CALLING"}
      "$($TenantName):MCOPSTN1" {return "SKYPE FOR BUSINESS PSTN DOMESTIC CALLING"}
      "$($TenantName):MCOPSTN5" {return "SKYPE FOR BUSINESS PSTN DOMESTIC CALLING (120 Minutes)"}
      "$($TenantName):VISIOONLINE_PLAN1" {return "VISIO ONLINE PLAN 1"}
      "$($TenantName):VISIOCLIENT" {return "VISIO Online Plan 2"}
      "$($TenantName):WIN10_PRO_ENT_SUB" {return "WINDOWS 10 ENTERPRISE E3"}
      "$($TenantName):WIN10_VDA_E5" {return "Windows 10 Enterprise E5"}
      "$($TenantName):WINDOWS_STORE" {return "WINDOWS STORE FOR BUSINESS"}
      "$($TenantName):" {return ""}
      "$($TenantName):DYN365_ENTERPRISE_P1_IW" {return "D365 ETR P1"}
      #"$($TenantName):DYN365_RETAIL_TRIAL" {return "D365 CRM TRIAL"}
      #"$($TenantName):MICROSOFT_BUSINESS_CENTER" {return "MBC"}
      #"$($TenantName):POWERAPPS_INDIVIDUAL_USER" {return "PAPPS IND User"}
      #"$($TenantName):POWERAPPS_VIRAL" {return "PAPPS and LOGIC FLOW"}
      #"$($TenantName):STREAM" {return "STREAM"}
      #"$($TenantName):AX7_USER_TRIAL" {return "D_AX7.0 TRIAL"}
      default {return $SkuId.Replace("$($TenantName):","")}
   }

}

function Get-LAPATH{
<#
.Synopsis
   Get-LAPATH is short for Get-LicenseAssingmentPaths function.
.DESCRIPTION
   Long description
.EXAMPLE
   get-LAPATH -UPN bandit@contoso.onmicrosoft.com
 
   This shows the licesning plans assigned to bandit in the contoso online tenant.
.EXAMPLE
   get-LAPATH -UPN bob@tailspintoys.com
 
   This shows the licesning plans assigned to bob in the tailspintoys online tenant.
.INPUTS
   UPN value or values can be used as input.
.OUTPUTS
   Screen outputs
.NOTES
   start-process https://agbo.blog/2019/08/22/how-is-your-office-365-users-licenses-assigned-direct-or-inherited/
   Author: Jean-Marie AGBO
   Updated: Mike O'Neill
.COMPONENT
   Need to run Connect-MSOLService in order to get Azure AD information from the MS Online realm.
#>


   [CmdletBinding()]
   Param(
       [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
       [string]$UPN
   )

   Begin{
       Get-Date
       Write-Host "## Data processing stated at $(Get-date)" -ForegroundColor Yellow
       Write-Host ""
       $TenantName = ((Get-MsolAccountSku).AccountSkuId[0] -split(':'))[0]
   }

   Process{

       Write-Host ""
       Write-Host "Working on $UPN" -ForegroundColor Green
       $User = Get-MsolUser -UserPrincipalName $UPN

       #Getting assignment paths
       $LicensesTab = $null
       $LicensePlan = $null
       $LicTabCount = 0
       $LicensesTab = $User.Licenses | Select-Object AccountSkuId, GroupsAssigningLicense

       if($LicensesTab){

           Write-Host "License Enabled : True" -ForegroundColor Yellow

           $i = 0 #(Measure-Object -InputObject $LicensesTab).Count
           $LicTabCount = $LicensesTab.AccountSkuId.Count

           Do{

               #Getting License Plan
               $LicensePlan = Get-LicensePlan -SkuId $LicensesTab[$i].AccountSkuId -TenantName $TenantName

               #Getting License Paths
               [System.Collections.ArrayList]$LicensePath = @()

               if($LicensesTab[$i].GroupsAssigningLicense){

                   foreach ($Guid in $LicensesTab[$i].GroupsAssigningLicense.guid){

                       if($Guid -eq $User.ObjectId.Guid){
                           $LicensePath.Add("Direct") | Out-Null
                       }
                       else{
                           $LicensePath.Add((Get-MsolGroup -ObjectId $Guid).DisplayName) | Out-Null
                       }

                   }
               }
               else{
                   $LicensePath.Add("Direct") | Out-Null
               }

               Write-Host "$LicensePlan : $([String]::Join(",",$LicensePath.ToArray()))" -ForegroundColor Yellow
               $i++

           }
           While ($i -ne $LicTabCount)
       }
       else {
           Write-Host "License Enabled : false" -ForegroundColor Red
       }
   }

   End{
       Write-Host ""
       Write-Host "## Data Processing ended on $(Get-Date)" -ForegroundColor Yellow
   }

}

<#
Function New-item {}
 
#>

function Get-O365IndividualPlanSettingsUser {
<#
.Synopsis
   List individual plans assigned to specific user
.DESCRIPTION
   This function is used to obtain individual plans assigned to an O365 user account.
.EXAMPLE
   Get-O365IndividualPlanSettingsUser -UPN bandit@contoso.com
 
   This will list out the individual license plans assigned to the user 'bandit' at the contoso.com tenant domain.
.EXAMPLE
   Get-O365IndividualPlanSettingsUser -UPN user31@tailspintoys.com
 
   This will list out the individual license plans assigned to the 'use31' at the tailspintoys.com tenant domain.
.INPUTS
   UPN Parameter
.OUTPUTS
   Screen output by default.
.NOTES
   Code from MS Docs:
   Start-Process https://docs.microsoft.com/en-us/microsoft-365/enterprise/view-account-license-and-service-details-with-microsoft-365-powershell?view=o365-worldwide
   Functioned up by Mike O'Neill
 
.COMPONENT
   Need to run Connect-MSOLService in order to get Azure AD information from the MS Online realm.
#>

   [cmdletbinding()]
   param ([parameter(mandatory=$false)] $UPN="bandit@mcdeo.onmicrosoft.com")


   $AllLicenses=(Get-MsolUser -UserPrincipalName $UPN).Licenses
   $licArray = @()
   for($i = 0; $i -lt $AllLicenses.Count; $i++)
   {
      $licArray += "License: " + $AllLicenses[$i].AccountSkuId
      $licArray +=  $AllLicenses[$i].ServiceStatus
      $licArray +=  ""
      $licArray
   }
}

<#
Get-O365IndividualPlanSettingsUser
#>


#endregion Viewing User values of direct assigned O365 licensing and plans within O365/M365


#region Test-HAFNIUM


function Get-26855() {
   Write-Host "Checking for CVE-2021-26855 in the HttpProxy logs"
   $files = (Get-ChildItem -Recurse -Path "$exchangePath\Logging\HttpProxy" -Filter '*.log').FullName
   $count = 0
   $allResults = @()
   $files | ForEach-Object {
        $count++
        Write-Progress -Activity "Checking for CVE-2021-26855 in the HttpProxy logs" -Status "$count / $($files.Count)" -PercentComplete ($count * 100 / $files.Count)
        if ((Get-ChildItem $_ -ErrorAction SilentlyContinue | Select-String "ServerInfo~").Count -gt 0) {
           $fileResults = @(Import-Csv -Path $_ -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -like 'ServerInfo~*/*' })
           $fileResults | ForEach-Object {
                $allResults += $_
               }
            }
    }

    if ($allResults.Length -gt 0) {
       Write-Warning "Suspicious entries found in $exchangePath\Logging\HttpProxy. Check the .\CVE-2021-26855.csv log for specific entries."
        if (Test-Path "$PSScriptRoot\CVE-2021-26855.log") {
           Remove-Item $PSScriptRoot\CVE-2021-26855.log -Force
         }
        $allResults | Select-Object DateTime, AnchorMailbox | Export-Csv $PSScriptRoot\CVE-2021-26855.log
    } else {
       Write-Host "No suspicious entries found." -ForegroundColor Green
    }
   }

function Get-26858() {
   Write-Host "`r`nChecking for CVE-2021-26858 in the OABGenerator logs"
    $logs = Get-ChildItem -Recurse -Path "$exchangePath\Logging\OABGeneratorLog" | Select-String "Download failed and temporary file" -List | Select-Object Path
    if ($logs.Path.Count -gt 0) {
        Write-Warning "Suspicious OAB download entries found in the following logs, please review them for `"Download failed and temporary file`" entries:"
        $logs.Path
      } else {
         Write-Host "No suspicious entries found." -ForegroundColor Green
    }
   }

function Get-26857() {
    Write-Host "`r`nChecking for CVE-2021-26857 in the Event Logs"
    $eventLogs = Get-EventLog -LogName Application -Source "MSExchange Unified Messaging" -EntryType Error -ErrorAction SilentlyContinue | Where-Object { $_.Message -like "*System.InvalidCastException*" }
    if ($eventLogs.Count -gt 0) {
       Write-Warning "Suspicious event log entries for Source `"MSExchange Unified Messaging`" and Message `"System.InvalidCastException`" were found. These may indicate exploitation. Please review these event log entries for more details."
      } else {
         Write-Host "No suspicious entries found." -ForegroundColor Green
    }
   }

   function Get-27065() {
      Write-Host "`r`nChecking for CVE-2021-26857 in the ECP Logs"
      $logs = Get-ChildItem -Recurse -Path "$exchangePath\Logging\ECP\Server\*.log" | Select-String "Set-.*VirtualDirectory" -List | Select-Object Path
    if ($logs.Path.Count -gt 0) {
       Write-Warning "Suspicious virtual directory modifications found in the following logs, please review them for `"Set-*VirtualDirectory`" entries:"
        $logs.Path
      } else {
        Write-Host "No suspicious entries found." -ForegroundColor Green
      }
}

function Get-SuspiciousFiles() {
   Write-Host "`r`nChecking for suspicious files"
    $lsassFiles = Get-ChildItem -Recurse -Path "$env:WINDIR\temp\lsass.*dmp"
    $lsassFiles += Get-ChildItem -Recurse -Path "c:\root\lsass.*dmp"
    if ($lsassFiles.Count -gt 0) {
       Write-Warning "lsass.exe dumps found, please verify these are expected:"
       $lsassFiles.FullName
    } else {
       Write-Host "No suspicious lsass dumps found." -ForegroundColor Green
    }

    $zipFiles = Get-ChildItem -Recurse -Path "$env:ProgramData" -Include *.7z, *.zip, *.rar -ErrorAction SilentlyContinue
    if ($zipFiles.Count -gt 0) {
       Write-Warning "`r`nZipped files found in $env:ProgramData, please verify these are expected:"
        $zipFiles.FullName
      } else {
         Write-Host "`r`nNo suspicious zip files found." -ForegroundColor Green
    }
   }


function Test-HAFNIUM {
   [cmdletbinding(HelpUri = 'https://mikeoneill.blog/exchange_addin/test-hafnium/')]
   param(
   [parameter(Mandatory=$true,
   HelpMessage="Enter single server name for remote PowerShell access.")] $ServerName)
   Write-Host "This function checks for exploits using the instructions outlined in https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers`r`n"

   $global:exchangePath = (Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ExchangeServer\v15\Setup).MsiInstallPath

   Write-Verbose "`$Servername variable contains $ServerName"
   $CredintalHAFNIUM = Get-Credential
   $session = New-PSSession -ComputerName $ServerName -Credential $CredintalHAFNIUM

Invoke-Command -SessionName $session -Credential $CredintalHAFNIUM -ScriptBlock {
   Get-26855
   Get-26858
   Get-26857
   Get-27065
   Get-SuspiciousFiles
}

}
#endregion Test-HAFNIUM