Exchange_AddIn.psm1


<#-----------------------------------------------------------------------------
PowerShell Module 'Exchange_AddIn'
 
Mike O'Neill, Microsoft Senior Premier Field Engineer
http://blogs.technet.microsoft.com/mconeill
 
Blog post of this module:
 
Generated on: 6/21/2016
 
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 adminstrate Exchange on premises, Exchange online, and Exchange hybrid environments.
.DESCRIPTION
    This is a compilation of scripts and functions packaged into a single module with verb-noun cmdlets for engineers to easily run.
.NOTES
    Current Version : 1.0
     
    History : 1.0 - Posted ?/??/2017 - First iteration
 
    Rights Required : Exchange Organization Administrator
.LINK
     
.FUNCTIONALITY
   This module assists Exchange engineers with a single point of reference, 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.
.SYNTAX
    None
.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
.INPUTS
    Requests users' credentials
.OUTPUTS
    None
.FUNCTIONALITY
   Prompts for new onpremises 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
   To connect up to an Exchange server on premises and not use cmdlet prefix option.
     
   Connect-ExchangeServer -computer NameOfExchangeServer
.EXAMPLE
   To connect up to an Exchange server on premises and use cmdlet prefix option of your choice.
 
   Connect-ExchangeServer -computer NameOfExchangeServer -Prefix OnPrem
.INPUTS
   None
.OUTPUTS
   None
.NOTES
   General notes
.ROLE
   Exchange servers.
.FUNCTIONALITY
   Connects remotely to an on premises Exchange server.
#>
  
[cmdletbinding()] 
    param ([parameter(Mandatory=$true)] $computer="lab1-ex2016",
           [parameter(Mandatory=$false)]$Prefix = $Null,
           $Global:OnPremisesComputer = $Computer,
           $Global:OnPremisesPrefix = $Prefix
          )

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

If ($Global:OnPremisesPrefix -eq $Null) {       
        $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$OnPremisesComputer/PowerShell/" -Authentication Kerberos -Credential $Global:OnPremisesCredential 
        Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking
    }
Else {
        $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$OnPremisesComputer/PowerShell/" -Authentication Kerberos -Credential $Global:OnPremisesCredential 
        Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking -Prefix $OnPremisesPrefix
    }
}
#endregion Connect to Exchange on-premises server

#region Exchange Online sign in

Function Request-CredentialExchangeOnline {
<#
.Synopsis
   Prompts user for a new user name and password for logon to tenant.
.SYNTAX
    None
.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-Credential
.INPUTS
    None
.OUTPUTS
    None
.FUNCTIONALITY
   Prompts for new O365 credentials.
#>

[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-ExchangeOnline {
<#
.SYNOPSIS
   Creates a PSSession to connect to Exchange Online.
.SYNTAX
    None
.DESCRIPTION
   This cmdlet combines the steps to successfully sign into an Exchange online tenant.
.EXAMPLE
   To connect up to an Exchange online tenant and not use cmdlet prefix option.
     
   Connect-ExchangeOnline
.EXAMPLE
   To connect up to an Exchange online tenant and use cmdlet prefix option.
     
   Connect-ExchangeOnline -prefix O365
.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()]
param ($Prefix = $Null,
       $Global:EXOPrefix = $Prefix
      )

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

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

    $myMBX = Get-MailBox $EXOCredential.UserName
      
    Write-Host  "`nHello $($myMBX), you are now logged into Exchange Online." -ForegroundColor Green
    }
else {

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

    $myMBX = Get-MailBox $EXOCredential.UserName
      
    Write-Host  "`nHello $($myMBX), you are now logged into Exchange Online." -ForegroundColor Green
    }
}

Function Disconnect-ExchangeOnline {
<#
.Synopsis
   Disconnects a PSSession from Exchange Online.
.SYNTAX
    None
.DESCRIPTION
   This cmdlet disconnects the sign-in session to an Exchange online tenant.
.EXAMPLE
   Disconnect-ExchangeOnline
.INPUTS
    None
.OUTPUTS
    None
.FUNCTIONALITY
   Terminates the PSSession that is connected to an O365 Exchange online remote session and clears the credential variable.
#>


[cmdletbinding()]
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

Function Get-DatabaseInformation { #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-DatabaseAvailabilityGroup"
.PARAMETER
    $OldPreference = $ErrorActionPreference
.PARAMETER
    $ErrorActionPreference = ‘stop’
.EXAMPLE
   Get-DatabaseInformation
.INPUTS
   None
.OUTPUTS
   None
.NOTES
   General notes
.COMPONENT
   None
.ROLE
   Exchange servers.
.FUNCTIONALITY
   Lists current Databases, what the status of each database is, and how many copies are available for each Database.
#>

[cmdletbinding()]
 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 {$_.Servers | ForEach {Test-ReplicationHealth -Server $_}}
        }
    }

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

Finally {$ErrorActionPreference=$oldPreference}

}

#endregion Test database status and present information of DB's

#region Obtain .Net version currently installed on OS.

Function Get-DotNETVersion {
<#
.Synopsis
   Displays .NET version on a computer.
.SYNTAX
    None
.DESCRIPTION
   This cmdlet finds 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.
.PARAMETER
    $Computer
    $RemoteComputer
.EXAMPLE
   Get-DotNETVersion
.EXAMPLE
   Get-DotNETVersion -RemoteComputer ServerToTest
.INPUTS
    None
.OUTPUTS
    None
.FUNCTIONALITY
   Displays the current release value and version of .NET.
#>


    [cmdletbinding(HelpURI="http://blogs.technet.microsoft.com/mconeill")]
    Param ($Computer,
      $RemoteComputer = $Computer)

    If ($RemoteComputer -ne $null) {
    New-PSSession -ComputerName $RemoteComputer
}
        $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: https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx
    $NetValue | Select Release, @{
        name="Product"
        expression={
      switch($_.Release) {
        378389 { [Version]"4.5" }
        378675 { [Version]"4.5.1" }
        378758 { [Version]"4.5.1" }
        379893 { [Version]"4.5.2" }
        393295 { [Version]"4.6" }
        393297 { [Version]"4.6" }
        394254 { [Version]"4.6.1" }
        394271 { [Version]"4.6.1" } 
        394802 { [Version]"4.6.2" }
        394806 { [Version]"4.6.2" }
        460798 { [Version]"4.7" }
        460805 { [Version]"4.7" }  
        default {"Unknown"}
      }
    }
}

}

#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 2013/2016 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()]
[OutputType([int])]
Param
(
    # determine what server to put in maintenance mode
    [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
    [string]$Server=$env:COMPUTERNAME,

    [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 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 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 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 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 "-----------------------`n`n" -ForegroundColor Yellow
            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 2013/2016 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()]
[OutputType([int])]
Param
(
    # determine what server to put in maintenance mode
    [Parameter(Mandatory=$true,
               ValueFromPipelineByPropertyName=$true,
               Position=0)]
    [string]$Server
)

$discoveredServer = Get-ExchangeServer -Identity $Server | Select 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 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 "-----------------------`n" -ForegroundColor Green
        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 "`nINFO: Done! Server $server successfully taken out of Maintenance Mode.`n" -ForegroundColor Green

$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