InstallExchange.psm1

<#
    Original credit for the majority of the logic in this module goes to:
 
    Michel de Rooij
    michel@eightwone.com
    http://eightwone.com
 
    And the AWS CloudFormation kickstarter for Exchange
#>


#region Constants
$script:LogPath = "$env:SystemDrive\InstallExchange.log"
$script:MajorOSVersion = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -Property @{Name = "Major"; Expression = {$_.Version.Split(".")[0] + "." +$_.Version.Split(".")[1]}} | Select-Object -ExpandProperty Major
$script:MinorOSVersion = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -Property @{Name = "Minor"; Expression = {$_.Version.Split(".")[2]}} | Select-Object -ExpandProperty Minor
$script:InstallExchangeTaskName = "InstallExchange"
$script:RunOnceTaskName = "InstallExchangeMonitor"

[System.Environment]::SetEnvironmentVariable("LogPath", $script:LogPath, [System.EnvironmentVariableTarget]::Machine)

$script:FilterPacks = @(
    @{
        "PackageId" = "{95140000-2000-0409-1000-0000000FF1CE}";
        "PackageName" = "Microsoft Office 2010 Filter Pack";
        "Url" = "http://download.microsoft.com/download/0/A/2/0A28BBFA-CBFA-4C03-A739-30CCA5E21659/FilterPack64bit.exe";
        "Arguments" = @("/q", "/norestart")
    },
    @{
        "PackageId" = "00004159000290400100000000F01FEC\Patches\2B24AAAA46EAEB942BF5566A6B1DE170";
        "PackageName" = "Microsoft Office 2010 Filter Pack SP1";
        "Url" = "http://download.microsoft.com/download/A/A/3/AA345161-18B8-45AE-8DC8-DA6387264CB9/filterpack2010sp1-kb2460041-x64-fullfile-en-us.exe";
        "Arguments" = @("/q", "/norestart")
    }
)

$script:WS2008R2Prereqs = @(
    @{
        "PackageId" = "KB974405";
        "PackageName" = "KB974405: Windows Identity Foundation";
        "Url" = "http://download.microsoft.com/download/D/7/2/D72FD747-69B6-40B7-875B-C2B40A6B2BDD/Windows6.1-KB974405-x64.msu";
        "Arguments" = @("/quiet", "/norestart")
    },
    @{
        "PackageId" = "KB2619234";
        "PackageName" = "KB2619234: Enable Association Cookie/GUID used by RPC/HTTP to also be used at RPC layer";
        "Url" = "http://hotfixv4.microsoft.com/Windows 7/Windows Server2008 R2 SP1/sp2/Fix381274/7600/free/437879_intl_x64_zip.exe";
        "Arguments" = @("/quiet", "/norestart")
    },
    @{
        "PackageId" = "KB2758857";
        "PackageName" = "KB2758857: Insecure library loading could allow remote code execution (supersedes KB2533623)";
        "Url" = "http://download.microsoft.com/download/A/9/1/A91A39EA-9BD8-422F-A018-44CD62CA7485/Windows6.1-KB2758857-x64.msu";
        "Arguments" = @("/quiet", "/norestart")
    }
)

$script:WS2012Prereqs = @(
    @{
        "PackageId" = "KB2985459";
        "PackageName" = "KB2985459: The W3wp.exe process has high CPU usage when you run PowerShell commands for Exchange";
        "Url" = "http://hotfixv4.microsoft.com/Windows 8/Windows Server 2012 RTM/nosp/Fix512067/9200/free/477081_intl_x64_zip.exe";
        "Arguments" = @("/quiet", "/norestart")
    },
    @{
        "PackageId" = "KB2884597";
        "PackageName" = "KB2884597: Virtual Disk Service or applications that use the Virtual Disk Service crash or freeze in Windows Server 2012";
        "Url" = "http://hotfixv4.microsoft.com/Windows 8 RTM/nosp/Fix469260/9200/free/467323_intl_x64_zip.exe";
        "Arguments" = @("/quiet", "/norestart")
    },
    @{
        "PackageId" = "KB2894875";
        "PackageName" = "KB2894875: Windows 8-based or Windows Server 2012-based computer freezes when you run the 'dir' command on an ReFS volume";
        "Url" = "http://hotfixv4.microsoft.com/Windows 8 RTM/nosp/Fix473391/9200/free/468889_intl_x64_zip.exe";
        "Arguments" = @("/quiet", "/norestart")
    }
)

$script:WS2012R2Prereqs = @()

$script:WS2016Prereqs = @()

$COUNTDOWN_TIMER                = 10
$DOMAIN_MIXEDMODE               = 0
$FOREST_LEVEL2003               = 2

# Minimum FFL/DFL levels
$EX2013_MINFORESTLEVEL          = 15137
$EX2013_MINDOMAINLEVEL          = 13236
$EX2016_MINFORESTLEVEL          = 15317
$EX2016_MINDOMAINLEVEL          = 13236

# Supported Exchange versions
$EX2013STOREEXE_RTM             = "15.00.0516.032"
$EX2013STOREEXE_CU1             = "15.00.0620.029"
$EX2013STOREEXE_CU2             = "15.00.0712.024"
$EX2013STOREEXE_CU3             = "15.00.0775.038"
$EX2013STOREEXE_SP1             = "15.00.0847.032"
$EX2013STOREEXE_CU5             = "15.00.0913.022"
$EX2013STOREEXE_CU6             = "15.00.0995.029"
$EX2013STOREEXE_CU7             = "15.00.1044.025"
$EX2013STOREEXE_CU8             = "15.00.1076.009"
$EX2013STOREEXE_CU9             = "15.00.1104.005"
$EX2013STOREEXE_CU10            = "15.00.1130.007"
$EX2013STOREEXE_CU11            = "15.00.1156.006"
$EX2013STOREEXE_CU12            = "15.00.1178.004"
#$EX2013STOREEXE_CU13 = "15.00.1210.003"
$EX2013STOREEXE_CU13            = "15.00.1210.000" #This matches the installer version
$EX2013STOREEXE_CU14            = "15.00.1236.000"
$EX2013STOREEXE_CU15            = "15.00.1263.000"

$EX2016STOREEXE_PRE             = "15.01.0225.016"
#$EX2016STOREEXE_RTM = "15.01.0225.042"
$EX2016STOREEXE_RTM             = "15.01.0225.037" #This matches the installer version
$EX2016STOREEXE_CU1             = "15.01.0396.030"
$EX2016STOREEXE_CU2             = "15.01.0466.034"
$EX2016STOREEXE_CU3             = "15.01.0544.027"
$EX2016STOREEXE_CU4             = "15.01.0669.032"

#Map of version numbers to their text based version
$Versions= @{ 
    $EX2013STOREEXE_RTM = "Exchange Server 2013 RTM";
    $EX2013STOREEXE_CU1 = "Exchange Server 2013 Cumulative Update 1";
    $EX2013STOREEXE_CU2 = "Exchange Server 2013 Cumulative Update 2";
    $EX2013STOREEXE_CU3 = "Exchange Server 2013 Cumulative Update 3";
    $EX2013STOREEXE_SP1 = "Exchange Server 2013 Service Pack 1";
    $EX2013STOREEXE_CU5 = "Exchange Server 2013 Cumulative Update 5";
    $EX2013STOREEXE_CU6 = "Exchange Server 2013 Cumulative Update 6";
    $EX2013STOREEXE_CU7 = "Exchange Server 2013 Cumulative Update 7";
    $EX2013STOREEXE_CU8 = "Exchange Server 2013 Cumulative Update 8";
    $EX2013STOREEXE_CU9 = "Exchange Server 2013 Cumulative Update 9";
    $EX2013STOREEXE_CU10 = "Exchange Server 2013 Cumulative Update 10";
    $EX2013STOREEXE_CU11 = "Exchange Server 2013 Cumulative Update 11";
    $EX2013STOREEXE_CU12 = "Exchange Server 2013 Cumulative Update 12";
    $EX2013STOREEXE_CU13 = "Exchange Server 2013 Cumulative Update 13";
    $EX2013STOREEXE_CU14 = "Exchange Server 2013 Cumulative Update 14";
    $EX2013STOREEXE_CU15 = "Exchange Server 2013 Cumulative Update 15";
    $EX2016STOREEXE_PRE = "Exchange Server 2016 Preview";
    $EX2016STOREEXE_RTM = "Exchange Server 2016 RTM";
    $EX2016STOREEXE_CU1 = "Exchange Server 2016 Cumulative Update 1";
    $EX2016STOREEXE_CU2 = "Exchange Server 2016 Cumulative Update 2";
    $EX2016STOREEXE_CU3 = "Exchange Server 2016 Cumulative Update 3";
    $EX2016STOREEXE_CU4 = "Exchange Server 2016 Cumulative Update 4"
}

# Exchange ISO Locations

$script:EX2016CU4_ISO = "https://download.microsoft.com/download/B/9/F/B9F59CF4-7C60-49EF-8A5B-8C2B7991FA86/ExchangeServer2016-x64-cu4.iso"
$script:EX2016CU3_ISO = "https://download.microsoft.com/download/4/C/E/4CE65F66-CE89-4F4D-96C0-A97E08FA1693/ExchangeServer2016-x64-cu3.iso"
$script:EX2016CU2_ISO = "https://download.microsoft.com/download/C/6/C/C6C10C1B-EFD8-4AE7-AEE1-C04F45869F5D/ExchangeServer2016-x64-CU2.iso"
$script:EX2016CU1_ISO = "https://download.microsoft.com/download/6/4/8/648EB83C-00F9-49B2-806D-E46033DA4AE6/ExchangeServer2016-CU1.iso"
$script:EX2016RTM_EXE = "https://download.microsoft.com/download/3/9/B/39B8DDA8-509C-4B9E-BCE9-4CD8CDC9A7DA/Exchange2016-x64.exe"
$script:EX2013CU15_EXE = "https://download.microsoft.com/download/3/A/5/3A5CE1A3-FEAA-4185-9A27-32EA90831867/Exchange2013-x64-cu15.exe"
$script:EX2013CU14_EXE = "https://download.microsoft.com/download/0/C/E/0CE142F1-E61D-4DBF-9436-334A4045A91F/Exchange2013-x64-cu14.exe"
$script:EX2013CU13_EXE = "https://download.microsoft.com/download/7/4/9/74981C3B-0D3C-4068-8272-22358F78305F/Exchange2013-x64-cu13.exe"
$script:EX2013CU12_EXE = "https://download.microsoft.com/download/2/C/1/2C151059-9B2A-466B-8220-5AE8B829489B/Exchange2013-x64-cu12.exe"

$script:Sources = @{
    "2013_CU12" = $script:EX2013CU12_EXE;
    "2013_CU13" = $script:EX2013CU13_EXE;
    "2013_CU14" = $script:EX2013CU14_EXE;
    "2013_CU15" = $script:EX2013CU15_EXE;

    "2016_RTM" = $script:EX2016RTM_EXE;
    "2016_CU1" = $script:EX2016CU1_ISO;
    "2016_CU2" = $script:EX2016CU2_ISO;
    "2016_CU3" = $script:EX2016CU3_ISO;
    "2016_CU4" = $script:EX2016CU4_ISO
}

# Supported Operating Systems
$WS2008R2_MAJOR                 = "6.1"
$WS2012_MAJOR                   = "6.2"
$WS2012R2_MAJOR                 = "6.3"
$WS2016_MAJOR                   = "10.0"

# .NET Versions
$script:NET45 = 378389
$script:NET451 = 378675
$script:NET452 = 379893
$script:NET46 = 393297
$script:NET461 = 394271



#endregion

Function Get-ExchangeInstallationMedia {
    <#
        .SYNOPSIS
            Downloads the specified Exchange installation media.
 
        .DESCRIPTION
            The cmdlet retrieves the installation media either from the internet or a specified AWS S3 bucket. The contents of the ISO or EXE are also automatically extracted to the destination directory.
 
        .PARAMETER Destination
            The location the ISO or EXE is downloaded to, should be a directory path. This defaults to "$env:SystemDrive\ExchangeSource". The contents of the ISO or EXE will also be extracted to this directory.
 
        .PARAMETER Source
            The URL to the installation media you want to download.
 
        .PARAMETER Version
            Specify the version of the installation media to download which uses preconfigured sources.
 
        .PARAMETER BucketName
            The AWS S3 bucket containing the installation media.
 
        .PARAMETER Key
            The S3 key of the installation media object.
 
        .PARAMETER PassThru
            Returns the destination the contents were downloaded to.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Get-ExchangeInstallationMedia -Version 2016_CU2
 
            Retrieves the installation media for Exchange 2016 CU2 and downloads it from the internet to the default destination.
 
        .EXAMPLE
            Get-ExchangeInstallationMedia -BucketName MyISOs -Key "Exchange/ExchangeServer2016-x64-CU2.iso"
 
            Downloads the ISO file from the MyISOs bucket, with a folder called Exchange in the bucket containing the ISO file. This method is intended to be used by an EC2 instance that is running
            with an IAM role that allows the file to download without credentials or from a public S3 bucket.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter()]
        [System.String]$Destination = "$env:SystemDrive\ExchangeSource",

        [Parameter(ParameterSetName="Source",Mandatory=$true)]
        [AllowEmptyString()]
        [System.String]$Source,

        [Parameter(ParameterSetName="Version", Mandatory=$true)]
        [ValidateSet("2013_CU12", "2013_CU13", "2013_CU14", "2013_CU15", "2016_RTM","2016_CU1","2016_CU2", "2016_CU3", "2016_CU4")]
        [System.String]$Version,

        [Parameter(ParameterSetName="AWS",Mandatory=$true)]
        [System.String]$BucketName,

        [Parameter(ParameterSetName="AWS",Mandatory=$true)]
        [System.String]$Key,

        [Parameter()]
        [switch]$PassThru
    )

    Begin {
    }

    Process {
        if (-not (Test-Path -Path $Destination)) 
        {
            Write-Log -Message "Creating download destination at $Destination."
            New-Item -Path $Destination -ItemType Directory -Force -Confirm:$false | Out-Null
        }
        else
        {
            if (-not [System.IO.Directory]::Exists($Destination))
            {
                $Msg = "Provided destination $Destination is an existing file."
                Write-Log -Message $Msg -Level ERROR

                throw $Msg
            }
        }

        if ($PSCmdlet.ParameterSetName -eq "AWS") 
        {
            Write-Log -Message "Downloading $Key from AWS S3 Bucket $BucketName."

            $Parts = $Key.Split("/")
            $FileName = $Parts[$Parts.Length - 1]
            $DownloadDestination = Join-Path -Path $Destination -ChildPath $FileName

            Import-Module -Name AWSPowerShell -ErrorAction Stop

            try 
            {
                Copy-S3Object -BucketName $BucketName -Key $Key -LocalFile "$DownloadDestination"
                Write-Log -Message "Successfully downloaded file from S3."
            }
            catch [Exception] 
            {
                if ($_.Exception.Message -eq "Access Denied") 
                {
                    Write-Log -Message "Received a 403 response from S3, this could be because the object $Key doesn't exist or the EC2 Instance doesn't have an IAM role with permission." -Level ERROR -ErrorRecord $_
                }
                else 
                {
                    Write-Log -Message "Error downloading object from S3." -Level ERROR -ErrorRecord $_
                }

                throw $_.Exception
            }
        }
        else 
        {
            if ($PSCmdlet.ParameterSetName -eq "Version") 
            {
                $Source = $script:Sources[$Version]
            }

            $WebClient = New-Object -TypeName System.Net.WebClient
            $Uri = New-Object -TypeName System.Uri($Source)
            $FileName = $Uri.Segments.Get($Uri.Segments.Count - 1)
            $Index = $Source.LastIndexOf("/")
            $BaseUrl = $Source.Substring(0, $Index)
            $DownloadDestination = Join-Path -Path $Destination -ChildPath $FileName

            try 
            {
                Register-ObjectEvent -InputObject $WebClient -EventName DownloadFileCompleted -SourceIdentifier Web.DownloadFileCompleted -Action {
                    $Global:DownloadComplete = $true
                } | Out-Null

                Register-ObjectEvent -InputObject $WebClient -EventName DownloadProgressChanged -SourceIdentifier Web.DownloadProgressChanged -Action {
                    $Global:Event = $event
                } | Out-Null

                Write-Log -Message "Downloading $FileName from $BaseUrl"
                $WebClient.DownloadFileAsync($Source, $DownloadDestination)

                $Counter = 0

                while (!$Global:DownloadComplete) 
                {
                    $Percent = $Global:Event.SourceArgs.ProgressPercentage
                    $TotalBytes = $Global:Event.SourceArgs.TotalBytesToReceive
                    $ReceivedBytes = $Global:Event.SourceArgs.BytesReceived

                    if ($Percent -ne $null) 
                    {
                        Write-Progress -Activity "Downloading $FileName from $BaseUrl" -Status "$ReceivedBytes bytes \ $TotalBytes bytes" -PercentComplete $Percent
                        
                        if ($Counter % 30 -eq 0) 
                        {
                            Write-Log -Message "Downloaded $ReceivedBytes bytes \ $TotalBytes bytes - $Percent%" -Level VERBOSE
                        }
                    }

                    Start-Sleep -Seconds 1 
                    $Counter++
                }

                Write-Progress -Activity "Downloading $FileName from $BaseUrl" -Status "$ReceivedBytes bytes \ $TotalBytes bytes" -Completed
                Write-Log -Message "Successfully completed download."
            }
            finally 
            {
                $WebClient.Dispose()
            }
        }

        [System.IO.FileInfo]$FileInfo = New-Object -TypeName System.IO.FileInfo($DownloadDestination)

        if ($FileInfo.Extension.ToLower() -eq ".iso") 
        {
            Write-Log -Message "Mounting ISO file."
            $Result = Mount-DiskImage -ImagePath $DownloadDestination -StorageType ISO -PassThru
            $Drive = $Result | Get-Volume | Select-Object -ExpandProperty DriveLetter
            Write-Log -Message "ISO mounted at drive $Drive`:\."
            
            #Use a job because the current PowerShell instance may not be able to access the mounted ISO drive
            $Job = Start-Job -ScriptBlock {
                $Counter = 0
                while (!(Test-Path -Path "$($args[0]):") -and $Counter -lt 60) 
                {
                    Start-Sleep -Seconds 1
                    $Counter++

                    if ($Counter -eq 60) 
                    {
                        Write-Log -Message "Error waiting for mounted ISO to become available." -Level ERROR
                        throw "Error waiting for mounted ISO to become available."
                    }
                }

                Write-Log -Message "Copying contents to $($args[1])."
                Copy-Item -Path "$($args[0]):\*" -Destination "$($args[1])" -Recurse
                Write-Log -Message "Copy completed."
            } -ArgumentList @($Drive, $Destination)

            Write-Log -Message "Waiting for extraction to complete..."
            Wait-Job -Job $Job  | Out-Null

            if ($Job.State -eq [System.Management.Automation.JobState]::Failed) 
            {
                $Msg = "Job to copy ISO contents failed with error: $($Job.ChildJobs[0].Error)"
                Write-Log -Message $Msg  -Level ERROR
                throw $Msg
            }

            Write-Log -Message "Unmounting ISO."
            Dismount-DiskImage -InputObject $Result
            Write-Log -Message "Deleting ISO."
            Remove-Item -Path $DownloadDestination -Confirm:$false -Force
        }

        #The provided destination parameter is a directory
        [System.IO.DirectoryInfo]$DirectoryInfo = New-Object -TypeName System.IO.DirectoryInfo($Destination)

        if ($DirectoryInfo.GetFiles().Length -eq 1) 
        {
            Write-Log -Message "Only 1 file was downloaded or extracted, going to unpack the single file."
            $Path = Get-ChildItem -Path $Destination -Filter "*.exe" | Select-Object -First 1 -ExpandProperty FullName
            Write-Log -Message "Extracting $Path"

            <#
                /a is unpack
                /q is quiet
                /x is the destination
            #>

            Start-Process -FilePath $Path -ArgumentList @("/a","/q","/x:`"$Destination`"") -Wait
            Write-Log -Message "Successfully extracted files."
            Write-Log -Message "Deleting self extracting cab file at $Path."
            Remove-Item -Path $Path -Confirm:$false -Force
        }

        if ($PassThru)
        {
            Write-Output -InputObject $DirectoryInfo.FullName
        }
    }

    End {
    }
}

Function Get-TextVersion {
    <#
        .SYNOPSIS
            Retrieves the text based version of Exchange based on the numeric version of the installer file.
 
        .DESCRIPTION
            Performs a lookup of the numeric version of the installer file to match it to the text based version.
 
        .PARAMETER FileVersion
            The version of the installer file to match against.
 
        .INPUTS
            System.String
 
                The version number as a string can be piped to the cmdlet.
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-TextVersion -FileVersion "15.01.0466.034"
 
            This returns "15.01.0466.034 Exchange Server 2016 Cumulative Update 2"
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
        [System.String]$FileVersion
    )

    Begin {
    }

    Process {
        if ($script:Versions.ContainsKey($FileVersion)) {
            $Result = "$FileVersion ($($Versions[$FileVersion]))"
        }
        else {
            $Result = "$FileVersion (Unknown Version)"
        }

        Write-Output -InputObject $Result
    }

    End {        
    }
}

Function Get-ForestRootNC {
    <#
        .SYNOPSIS
            Gets the Active Directory Forest Root Naming Context.
 
        .DESCRIPTION
            This cmdlet gets the Active Directory Forest Root Naming Context.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-ForestRootNC
 
            For the contoso.com forest root, this returns "DC=Contsos,DC=com".
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
    }

    Process {
        try 
        {
            Write-Log -Message "Getting forest root naming context." -Level VERBOSE
            [System.DirectoryServices.ActiveDirectory.Forest]$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $NamingContext = "DC=$($Forest.Name.Replace(".",",DC="))"
            Write-Log -Message "Naming context is $NamingContext." -Level VERBOSE
            Write-Output -InputObject $NamingContext
        }
        catch [Exception] 
        {
            Write-Log -Message "Could not retrieve the forest root naming context." -ErrorRecord $_ -Level ERROR
            Write-Output -InputObject $null
        }
    }
    
    End {
    }
}

Function Get-DomainNC {
    <#
        .SYNOPSIS
            Gets the Active Directory Domain Naming Context.
 
        .DESCRIPTION
            This cmdlet gets the Active Directory Domain Naming Context for the computer's current domain.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-DomainNC
 
            For the tailspintoys.com domain, this returns "DC=Tailspintoys,DC=com".
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
    }

    Process {
        try {
            Write-Log -Message "Getting domain root naming context." -Level VERBOSE
            [System.DirectoryServices.ActiveDirectory.Domain]$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
            $NamingContext = "DC=$($Domain.Name.Replace(".",",DC="))"
            Write-Log -Message "Naming context is $NamingContext." -Level VERBOSE
            Write-Output -InputObject $NamingContext
        }
        catch [Exception] {
            Write-Log -Message "Could not retrieve the domain root naming context." -ErrorRecord $_ -Level ERROR
            Write-Output -InputObject $null
        }
    }

    End {
    }
}

Function Get-ForestFunctionalLevel {
    <#
        .SYNOPSIS
            Gets the Active Directory Forest functional level.
 
        .DESCRIPTION
            This cmdlet gets the Active Directory Forest functional level.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Int
 
        .EXAMPLE
            Get-ForestFunctionalLevel
 
            Returns the integer value representing the current forest functional level.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
    }

    Process {
        try 
        {
            [System.DirectoryServices.ActiveDirectory.Forest]$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            Write-Output -InputObject $Forest.ForestModeLevel
        }
        catch [Exception] 
        {
            Write-Log "Could not retrieve forest functional level." -ErrorRecord $_ -Level ERROR
            Write-Output -InputObject $null
        }
    }

    End {
    }
}

Function Test-DomainNativeMode {
    <#
        .SYNOPSIS
            For a Windows 2000 Active Directory environment, tests if the Domain is running in native mode.
 
        .DESCRIPTION
            This cmdlet tests for Windows 2000 Domain native mode of the current domain.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-DomainNativeMode
 
            Returns true if the domain is not running Windows 2000 Mixed Mode.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        [System.DirectoryServices.ActiveDirectory.Domain]$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        Write-Output -InputObject ($Domain.DomainMode -ne [System.DirectoryServices.ActiveDirectory.DomainMode]::Windows2000MixedDomain)
    }
    
    End {}
}

Function Get-ExchangeOrganization {
    <#
        .SYNOPSIS
            Gets the Exchange Organization.
 
        .DESCRIPTION
            Retrieves the msExchOrganizationContainer object name from Active Directory. The cmdlet returns null of the object does not exist.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String or Null
 
        .EXAMPLE
            Get-ExchangeOrganization
 
            Returns the Exchange Organization name.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {        
    }

    Process {     
        Write-Log -Message "Getting Exchange Organization name from the msExchOrganizationContainer object class."

        $NC = Get-ForestRootNC

        if ($NC -ne $null)
        {
            try 
            {
                $Path = "LDAP://CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC"

                if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) 
                {
                    $ExOrgContainer = [ADSI]$Path
                    $Result = $ExOrgContainer.PSBase.Children | Where-Object { $_.objectClass -eq 'msExchOrganizationContainer' } | Select-Object -ExpandProperty Name
                }
                else 
                {
                    Write-Log -Message "Can't find Exchange Organization object" -Level VERBOSE
                    $Result = $null
                }
            }
            catch [Exception] 
            {
                Write-Log -Message "Can't find Exchange Organization object" -ErrorRecord $_ -Level VERBOSE
                $Result = $null
            }
        }
        else
        {
            Write-Log -Message "Forest naming context was null" -Level VERBOSE
            $Result = $null
        }
        
        Write-Output -InputObject $Result
    }
    
    End {        
    }
}

Function Test-ExchangeOrganization {
    <#
        .SYNOPSIS
            Tests for the existence of a specific Exchange Organization.
 
        .DESCRIPTION
            This cmdlet tests for the existence of the specified Exchange Organization.
 
        .PARAMETER Organization
            The organization name to test the existence of.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-ExchangeOrganization -Organization "contoso"
 
            Returns true if the contoso Exchange organization exists in Active Directory.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline = $true)]
        [System.String]$Organization
    )

    Begin {
    }

    Process {
        Write-Log -Message "Testing for the existence of the $Organization organization in Active Directory."

        $NC= Get-ForestRootNC

        if ($NC -ne $null)
        {
            $Path = "LDAP://CN=$Organization,CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC"
            $Result = [System.DirectoryServices.DirectoryEntry]::Exists($Path)
            Write-Output -InputObject $Result
        }
        else
        {
            Write-Log -Message "Forest root naming context was null." -Level VERBOSE
            Write-Output -InputObject $false
        }
    }

    End {    
    }
}

Function Get-ExchangeForestLevel {
    <#
        .SYNOPSIS
            Gets the current Exchange Forest level.
 
        .DESCRIPTION
            This cmdlet reads the ms-Exch-Schema-Version upperRange attribute.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-ExchangeForestLevel
 
            Returns the current Exchange environment forest level.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {      
        $NC= Get-ForestRootNC

        if ($NC -ne $null)
        {
            try
            {
                $Path = "LDAP://CN=ms-Exch-Schema-Version-Pt,CN=Schema,CN=Configuration,$NC"
                if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true)
                {
                    $Result = [ADSI]$Path | Select-Object -ExpandProperty rangeUpper
                }
                else 
                {
                    Write-Log -Message "$Path does not exist." -Level VERBOSE
                    $Result = $null
                }
            }
            catch [Exception] 
            {
                Write-Log -Message "Could not retrieve Exchange Forest Level." -ErrorRecord $_ -Level VERBOSE
                $Result = $null
            }
        }
        else
        {
            Write-Log -Message "Forest root naming context was null." -Level VERBOSE
            $Result = $null
        }

        Write-Output -InputObject $Result
    }

    End {        
    }
}

Function Get-ExchangeDomainLevel {
    <#
        .SYNOPSIS
            Gets the current Exchange Domain level.
 
        .DESCRIPTION
            This cmdlet reads the Microsoft Exchange System Objects objectVersion attribute.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-ExchangeDomainLevel
 
            Returns the current Exchange environment domain level.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        
        $NC = Get-DomainNC

        if ($NC -ne $null)
        {
            try 
            {
                $Path = "LDAP://CN=Microsoft Exchange System Objects,$NC"
                if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) 
                {
                    $Result = [ADSI]$Path | Select-Object -ExpandProperty objectVersion
                }
                else 
                {
                    $Result = $null
                }
            }
            catch [Exception] 
            {
                Write-Log -Message "Could not retrieve Exchange Domain Level." -ErrorRecord $_ -Level VERBOSE
                $Result = $null
            }
        }
        else
        {
            Write-Log -Message "Domain root naming context was null." -Level VERBOSE
            $Result = $null
        }

        Write-Output -InputObject $Result
    }

    End {    
    }
}

Function Remove-AutodiscoverServiceConnectionPoint {
    <#
        .SYNOPSIS
            Removes the Autodiscover Service Connection Point from Active Directory.
 
        .DESCRIPTION
            This cmdlet removes the serviceConnectionPoint object from Active Directory for Exchange Autodiscover.
 
        .PARAMETER Name
            The name of the service connection point.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Remove-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME
 
            Removes the autodiscover service connection point for the current server.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
        [System.String]$Name
    )

    Begin {
    }

    Process {
        Write-Log -Message "Removing the Autodiscover Service Connection Point from Active Directory." -Level VERBOSE
    
        $NC= Get-ForestRootNC

        if ($NC -ne $null)
        {

            $LDAPSearch= New-Object System.DirectoryServices.DirectorySearcher
            $LDAPSearch.SearchRoot= "LDAP://CN=Configuration,$NC"
            $LDAPSearch.Filter= "(&(cn=$Name)(objectClass=serviceConnectionPoint)(serviceClassName=ms-Exchange-AutoDiscover-Service)(|(keywords=67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68)(keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596)))"
        
            $LDAPSearch.FindAll() | ForEach-Object {
                Write-Log "Removing object $($_.Path)" -Level VERBOSE
                ([ADSI]($_.Path)).DeleteTree()
            }
        }
        else
        {
            Write-Log -Message "Forest root naming context was null." -Level VERBOSE
        }
    }

    End {
    }
}

Function Add-AutodiscoverServiceConnectionPoint {
    <#
        .SYNOPSIS
            Adds an Autodiscover Service Connection Point in Active Directory.
 
        .DESCRIPTION
            This cmdlet adds the serviceConnectionPoint object in Active Directory for Exchange Autodiscover.
 
        .PARAMETER Name
            The name of the service connection point.
 
        .PARAMETER ServiceBinding
            The FQDN of the Client Access Server.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Add-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME -ServiceBinding "https://$($ENV:COMPUTERNAME).contoso.com/autodiscover/autodiscover.xml"
 
            Adds the autodiscover service connection point for the current server.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true, Position = 0)]
        [System.String]$Name,

        [Parameter(Mandatory=$true, Position = 1)]
        [System.String]$ServiceBinding
    )

    Begin {}

    Process {
        $NC= Get-ForestRootNC

        if ($NC -ne $null)
        {
            $LDAPSearch= New-Object System.DirectoryServices.DirectorySearcher
            $LDAPSearch.SearchRoot= "LDAP://CN=Configuration,$NC"
            $LDAPSearch.Filter= "(&(cn=$Name)(objectClass=serviceConnectionPoint)(serviceClassName=ms-Exchange-AutoDiscover-Service)(|(keywords=67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68)(keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596)))"
        
            $LDAPSearch.FindAll() | ForEach-Object {
                Write-Log "Setting serviceBindingInformation on $($_.Path) to $ServiceBinding." -Level VERBOSE
            
                try 
                {
                    $SCPObj= $_.GetDirectoryEntry()
                    [void]$SCPObj.Put('serviceBindingInformation', $ServiceBinding)
                    $SCPObj.SetInfo()
                }
                catch [Exception] 
                {
                    Write-Log "Problem setting serviceBindingInformation property." -Level ERROR -ErrorRecord $_
                }
            }
        }
        else
        {
            Write-Log -Message "Forest root naming context was null." -Level VERBOSE
        }

    }

    End {
    }
}

Function Enable-IFilters {
    <#
        .SYNOPSIS
            Enables OneNote and Publisher IFilters in Exchange.
 
        .DESCRIPTION
            Enables OneNote and Publisher IFilters in Exchange.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Enable-IFilters
 
            Enables the OneNote and Publisher IFilters.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        # Note: Requires restarting "Microsoft Exchange Transport" and "Microsoft Filtering Management Service", but reboot will take care of that
        Write-Log -Message "Enabling OneNote and Publisher filtering" -Level VERBOSE
        
        $iFilterDirName = "$env:CommonProgramFiles\Microsoft Shared\Filters\"
        $KeyParent = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole"
        $CLSIDKey = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole\CLSID"
        $FiltersKey = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole\filters"
        $ONEFilterLocation = $iFilterDirName + "\ONIFilter.dll"
        $PUBFilterLocation = $iFilterDirName + "\PUBFILT.dll"
        $ONEGuid    ="{B8D12492-CE0F-40AD-83EA-099A03D493F1}"
        $PUBGuid    ="{A7FD8AC9-7ABF-46FC-B70B-6A5E5EC9859A}" 

        New-Item -Path $KeyParent -Name CLSID -ErrorAction SilentlyContinue -Force| Out-Null
        New-Item -Path $KeyParent -Name filters -ErrorAction SilentlyContinue -Force | Out-Null
        New-Item -Path $CLSIDKey -Name $ONEGuid -Value $ONEFilterLocation -Type String -Force| Out-Null
        New-Item -Path $CLSIDKey -Name $PUBGuid -Value $PUBFilterLocation -Type String -Force| Out-Null
        New-ItemProperty -Path "$CLSIDKey\$ONEGuid" -Name "ThreadingModel" -Value "Both" -Type String -Force| Out-Null
        New-ItemProperty -Path "$CLSIDKey\$PUBGuid" -Name "ThreadingModel" -Value "Both" -Type String -Force| Out-Null
        New-ItemProperty -Path "$CLSIDKey\$ONEGuid" -Name "Flags" -Value "1" -Type Dword -Force| Out-Null
        New-ItemProperty -Path "$CLSIDKey\$PUBGuid" -Name "Flags" -Value "1" -Type Dword -Force| Out-Null
        New-Item -Path $FiltersKey -Name ".one" -Value $ONEGuid -Type String -Force| Out-Null
        New-Item -Path $FiltersKey -Name ".pub" -Value $PUBGuid -Type String -Force| Out-Null 

        $Acl = Get-Acl -Path $KeyParent

        [System.Security.Principal.SecurityIdentifier]$NetworkService = New-Object -TypeName System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::NetworkServiceSid, $null)

        [System.Security.AccessControl.RegistryAccessRule]$Rule = New-Object -TypeName System.Security.AccessControl.RegistryAccessRule($NetworkService.Translate([System.Security.Principal.NTAccount]),
            [System.Security.AccessControl.RegistryRights]::ReadKey,
            [System.Security.AccessControl.AccessControlType]::Allow)

        $Acl.SetAccessRule($Rule)
        $Acl | Set-Acl -Path $KeyParent

        Write-Warning -Message "`"Microsoft Exchange Transport`" and `"Microsoft Filtering Management Service`" must be restarted for this to take effect."
    }

    End {
    }
}
       
Function Set-DisableSharedCacheServiceProbe {
    <#
        .SYNOPSIS
            Runs the contents of KB2971467 to disable the shared cache service probe.
 
        .DESCRIPTION
            Taken from DisableSharedCacheServiceProbe.ps1. Copyright (c) Microsoft Corporation. All rights reserved.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-DisableSharedCacheServiceProbe
 
            Disables the shared cache service probe.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        Write-Log -Message "Applying DisableSharedCacheServiceProbe (KB2971467, 'Shared Cache Service Restart' Probe Fix)"
        
        $ExchangeInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue -Name MsInstallPath | Select-Object -ExpandProperty MsiInstallPath

        if (![System.String]::IsNullOrEmpty($ExchangeInstallPath) -and (Test-Path -Path "$ExchangeInstallPath")) 
        {           
            $ProbeConfigFile= Join-Path -Path "$ExchangeInstallPath" -ChildPath "Bin\Monitoring\Config\SharedCacheServiceTest.xml"
            
            if (Test-Path $ProbeConfigFile) {
                $Date = Get-Date -Format s
                $Ext = ".orig_" + $Date.Replace(':', '-');
                $Backup = $ProbeConfigFile + $Ext
                $XmlBackup = [XML](Get-Content -Path $ProbeConfigFile)
                $XmlBackup.Save($Backup)    
    
                $XmlDoc = [XML](Get-Content -Path $ProbeConfigFile)
                $Definition = $XmlDoc.Definition.MaintenanceDefinition
    
                if ($Definition -eq $null) 
                {
                    Write-Log -Message "KB2971467: Expected XML node Definition.MaintenanceDefinition.ExtensionAttributes not found. Skipping." -Level WARNING
                }
                else {
                    $Modified = $false

                    if ($Definition.Enabled -ne $null -and $Definition.Enabled -ne "false") 
                    {
                        $Definition.Enabled = "false"
                        $Modified = $true
                    }

                    if ($Modified -eq $true) 
                    {
                        $XmlDoc.Save($ProbeConfigFile)
                        Write-Log -Message "Finished KB2971467, Saved $ProbeConfigFile."
                    }
                    else 
                    {
                        Write-Log -Message "Finished KB2971467, No values modified."
                    }
                }
            }
            else 
            {
                Write-Log -Message "KB2971467: Did not find file in expected location, skipping $ProbeConfigFile." -Level WARNING
            }
        }
        else 
        {
            Write-Log -Message "KB2971467: Unable to locate Exchange install path" -Level WARNING
        }
    }

    End {}
}

Function Start-ExchangeCleanup {
    <#
        .SYNOPSIS
            Performs the necessary cleanup tasks after installing Exchange with this module.
 
        .DESCRIPTION
            This cmdlet removes any unneeded Windows Features, files, scheduled tasks, and RunOnce scripts after the Exchange installation.
 
        .PARAMETER WindowsFeatures
            The Windows features to uninstall.
 
        .PARAMETER Paths
            The list of files or folders to delete.
             
        .PARAMETER TaskName
            The name of the scheduled task for an unattended installation to remove. This defaults to $script:InstallExchangeTaskName which is InstallExchange.
             
        .PARAMETER RunOnceTaskName
            The name of the RunOnce script to remove. This defaults to $script:RunOnceTaskName which is InstallExchangeMonitor.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-ExchangeCleanup -Paths @("c:\exchangetemp")
 
            Runs the cleanup tasks and deletes the folder c:\exchangetemp.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/26/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter()]
        [System.String[]]$WindowsFeatures,

        [Parameter()]
        [System.String[]]$Paths,

        [Parameter()]
        [System.String]$TaskName = $script:InstallExchangeTaskName,

        [Parameter()]
        [System.String]$RunOnceTaskName = $script:RunOnceTaskName
    )

    Begin {        
    }

    Process {
        if ([System.String]::IsNullOrEmpty($TaskName))
        {
            $TaskName = $script:InstallExchangeTaskName
        }

        if ([System.String]::IsNullOrEmpty($RunOnceTaskName)) {
            $RunOnceTaskName = $script:RunOnceTaskName
        }

        Write-Log -Message "Cleaning up..."
        foreach ($Item in $WindowsFeatures) {
            if (Get-WindowsFeature -Name $Item -ErrorAction SilentlyContinue) {
                try {
                    Write-Log -Message "Removing Windows Feature: $Item."
                    Remove-WindowsFeature -Name $Item -Confirm:$false
                }
                catch [Exception] {
                    Write-Log -Message "Error removing $Item" -Level ERROR -ErrorRecord $_
                }
            }
        }

        foreach ($Item in $Paths) {
            Write-Log -Message "Removing $Item" -Level VERBOSE
            Remove-Item -Path $Item -Force -Confirm:$false -ErrorAction SilentlyContinue -Recurse
        }

        Write-Log -Message "Removing scheduled task $TaskName."
        if ((Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) -ne $null) {
            Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
            Write-Log -Message "Successfully removed scheduled task."
        }
        else {
            Write-Log -Message "No scheduled task matching $TaskName present."
        }

        try {
            Write-Log -Message "Removing RunOnce scripts."
            $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
            Remove-ItemProperty -Path $Path -Name $RunOnceTaskName -ErrorAction SilentlyContinue
            Write-Log -Message "Successfully removed RunOnce command."
        }
        catch [Exception] {
            Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level WARNING
        }

        Write-Log -Message "Successfully finished cleanup."
    }

    End {        
    }
}

Function Start-ExchangeFixIt {
    <#
        .SYNOPSIS
            Launches Microsoft published Exchange FixIt scripts.
 
        .DESCRIPTION
            This cmdlet runs the specified Exchange FixIt script contents.
 
            Taken from Exchange2013-KB2938053-FixIt.ps1
            Parts taken from Exchange2013-KB2997355-FixIt.ps1
            Copyright (c) Microsoft Corporation. All rights reserved.
 
        .PARAMETER KB
            The KB # of the FixIt to run.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-ExchangeFixIt -KB KB2997355
 
            Runs the KB2997355 FixIt script.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/26/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
        [ValidateSet("KB2938053", "KB2997355")]
        [System.String]$KB
    )

    Begin {
    }

    Process {
        switch ($KB) {
            "KB2938053" {

                Write-Log -Message "Applying Exchange2013-KB2938053-FixIt (KB2938053, Transport Agent Fix)"

                $BaseDirectory = "$env:windir\Microsoft.NET\assembly\GAC_MSIL"
                $PolicyDirectories = @{ "policy.14.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.0.cfg";
                        "policy.14.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.0.cfg";
                        "policy.14.1.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.1.cfg";
                        "policy.14.1.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.1.cfg";
                        "policy.14.2.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.2.cfg";
                        "policy.14.2.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.2.cfg";
                        "policy.14.3.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.3.cfg";
                        "policy.14.3.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.3.cfg";
                        "policy.14.4.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.4.cfg";
                        "policy.14.4.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.4.cfg";
                        "policy.15.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy15.0.cfg";
                        "policy.15.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy15.0.cfg";
                        "policy.8.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy.cfg";
                        "policy.8.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy.cfg";
                        "policy.8.1.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.1.cfg";
                        "policy.8.1.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.1.cfg";
                        "policy.8.2.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.2.cfg";
                        "policy.8.2.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.2.cfg";
                        "policy.8.3.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.3.cfg";
                        "policy.8.3.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.3.cfg"; }

                $Configs = @()
                foreach ($Key in $PolicyDirectories.Keys) {
                    $Configs += Get-ChildItem -Path (Join-Path -Path $BaseDirectory -ChildPath $Key) -Recurse -Filter $PolicyDirectories[$Key] | Select-Object -ExpandProperty FullName
                }

                $Count = 0;
                foreach ($File in $Configs) {
                    Write-Log -Message "Fixing $File..." -Level VERBOSE
                    $Content = Get-Content -Path $File
                    $Content -replace "[-\d+\.]*-->","-->" | Out-File $File -Force -Confirm:$false
                    $Count++
                }

                Write-Log -Message "Exchange2013-KB2938053-FixIt fixed $Count files."
                break
            }
            "KB2997355" {
                Write-Log -Message "Applying Exchange2013-KB2997355-FixIt (KB2997355, Exchange Online Mailbox Management Fix)."
                $ExchangeInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue -Name MsiInstallPath | Select-Object -ExpandProperty MsiInstallPath
                if (![System.String]::IsNullOrEmpty($ExchangeInstallPath) -and (Test-Path -Path $ExchangeInstallPath)) {
                    $XConfigFile = Join-Path -Path (Join-Path -Path $ExchangeInstallPath -ChildPath "ClientAccess\ecp\DDI") -ChildPath "RemoteDomains.xaml"

                    Write-Log -Message "KB2997355: Updating XAML file $XConfigFile..."
                    $Content = Get-Content -Path "$XConfigFile"
                    $Content = $Content -Replace '<Variable DataObjectName="RemoteDomain" Name="DomainName" Type="{x:Type s:String}" />','<Variable DataObjectName="RemoteDomain" Name="DomainName" Type="{x:Type s:String}" /> <Variable DataObjectName="RemoteDomain" Name="TargetDeliveryDomain" Type="{x:Type s:Boolean}" />' 
                    $Content = $Content -Replace '<GetListWorkflow Output="Identity, Name, DomainName">','<GetListWorkflow Output="Identity, Name, DomainName, TargetDeliveryDomain">'
                    $Content = $Content -Replace '<GetObjectWorkflow Output="Identity,Name, DomainName, AllowedOOFType, AutoReplyEnabled,AutoForwardEnabled,DeliveryReportEnabled, NDREnabled, TNEFEnabled, MeetingForwardNotificationEnabled, CharacterSet, NonMimeCharacterSet">','<GetObjectWorkflow Output="Identity, Name, DomainName, TargetDeliveryDomain, AllowedOOFType, AutoReplyEnabled, AutoForwardEnabled, DeliveryReportEnabled, NDREnabled, TNEFEnabled, MeetingForwardNotificationEnabled, CharacterSet, NonMimeCharacterSet">'
                    $Content | Out-File "$XConfigFile" -Force -Confirm:$false
                    # IISReset not required at this stage
                    Write-Log -Message "KB2997355: Fixed XAML files"
                }
                else {
                    Write-Log -Message 'KB2997355: Unable to locate Exchange install path' -Level WARNING
                }
                break
            }
            default {
                throw "Could not determine the selected KB to run the fix it."
                break
            }

        }
    }

    End {        
    }
}

Function Test-ExchangeReadiness {
    <#
        .SYNOPSIS
            Tests the readiness of the server and Active Directory for the Exchange installation.
 
        .DESCRIPTION
            This cmdlet ensures all of the prerequisites for installing Exchange are in place. This includes:
 
                -Ensuring the temp directory is available for installation files
                -Verifying the OS version
                -Ensuring admin credentials
                -Access to the setup.exe for Exchange
                -Domain membership
                -Credential validation
                -Required components of the config file
                -Domain and forest functional levels
 
        .PARAMETER Config
            The generated config object with all of the specified parameters to run this module's installation cmdlet.
 
        .PARAMETER Credential
            The credentials to be used during an unattended installation.
 
        .INPUTS
            System.Object
                     
                The config object can be piped to this cmdlet.
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-ExchangeReadiness -Config $Config -Credential $Credential
 
            Tests the environment's and config's readiness to deploy exchange.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline = $true, Position = 0)]
        [System.Object]$Config,

        [Parameter()] 
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {    
    }

    Process {
        $MajorSetupVersion = [System.Decimal]"$($Config.SetupVersion.Split(".")[0]).$($Config.SetupVersion.Split(".")[1])"

        Write-Log -Message "Performing sanity checks."

        Write-Log -Message "Checking temporary installation folder." -Level VERBOSE

        if (!(Test-Path -Path "$($Config.TempDirectory)")) 
        {
            try 
            {
                New-Item -Path "$($Config.TempDirectory)" -ItemType Directory | Out-Null
            }
            catch [Exception] 
            {
                $Msg = "Error creating Temporary Installation Directory at $($Config.TempDirectory)."
                throw $Msg
            }
        }

        Write-Log -Message "Checking Operating System $($MajorOSVersion).$($MinorOSVersion)" -Level VERBOSE

        if (($MajorOSVersion -ne $WS2012R2_MAJOR) -and ($MajorOSVersion -ne $WS2012_MAJOR) -and ($MajorOSVersion -eq $WS2008R2_MAJOR -and $MinorOSVersion -lt 7601)) 
        {
            $Msg = "Windows Server 2008 R2 SP1, Windows Server 2012 or Windows Server 2012 R2 is required, but not detected"
            throw $Msg
        }

        Write-Log -Message "Checking privilege elevation..."
        if (!(Test-IsLocalAdmin)) 
        {
            $Msg = "Script requires local admin privileges."
            throw $Msg
        }
        else 
        {
            Write-Log -Message "Credentials appear to be running with local administrator rights."
        }

        Write-Log -Message "Checking for access to Exchange setup.exe file."
            
        if (!(Test-Path -Path "$($Config.SourceDirectory)\setup.exe")) 
        {
            $Msg = "Can't find Exchange setup at $($Config.SourceDirectory)\setup.exe."
            throw $Msg
        }
        else 
        {
            Write-Log -Message "Exchange Setup Version: $(Get-TextVersion $Config.SetupVersion)." -Level VERBOSE
            Write-Log -Message "Checking roles to install." -Level VERBOSE

            #This is Exchange 2016 and only the mailbox role is supported
            if ($MajorSetupVersion -ge 15.01) 
            {
                if (!$Config.InstallMailbox) 
                {
                    $Msg = "No roles specified to install"
                    throw $Msg
                }
                
                if ($Config.InstallCAS) 
                {
                    Write-Log -Message "Exchange 2016 setup detected, will ignore deprecated InstallCAS parameter." -Level WARNING
                }
            }
            else 
            {
                if (!$Config.InstallMailbox -and !$Config.InstallCAS) 
                {
                    $Msg = "No roles specified to install"
                    throw $Msg
                }
            }
        }
        
        Write-Log -Message "Checking domain membership status..."
        if((Get-CimInstance -ClassName Win32_ComputerSystem).PartOfDomain -eq $false) 
        {
            $Msg = "System is not domain-joined"
            throw $Msg
        }

        Write-Log -Message "Checking NIC configuration..."
        if ((Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled = True and DHCPEnabled = False") -eq $null) 
        {
            Write-Log -Message "System doesn't have a static IP address configured." -Level WARNING
        }

        if ($Config.TargetDirectory) 
        {
            $Drive = Split-Path $Config.TargetDirectory -Qualifier
            Write-Log -Message "Checking installation target directory..."

            if(!(Test-Path -Path $Drive)) 
            {
                $Msg = "Target directory drive unavailable: ($Drive)"
                throw $Msg
            }
        }

        if ($Config.InstallMDBLogPath) 
        {
            $Drive = Split-Path $Config.InstallMDBLogPath -Qualifier

            Write-Log -Message "Checking MDB log path..."

            if(!(Test-Path -Path $Drive)) 
            {
                $Msg = "MDB log drive unavailable: ($Drive)"
                throw $Msg
            }
        }

        if ($Config.InstallMDBDBPath) {
            $Drive = Split-Path $Config.InstallMDBDBPath -Qualifier

            Write-Log -Message "Checking MDB database path..."

            if(!(Test-Path -Path $Drive)) 
            {
                $Msg = "MDB database drive unavailable: ($Drive)"
                throw $Msg
            }
        }

        $ExOrg = Get-ExchangeOrganization
        if (![System.String]::IsNullOrEmpty($ExOrg)) 
        {
            if(![System.String]::IsNullOrEmpty($Config.Organization)) 
            {
                if($ExOrg -ne $Config.Organization) 
                {
                    $Msg = "OrganizationName ($($Config.Organization)) mismatches with discovered Exchange Organization name ($ExOrg)."
                    throw $Msg
                }
            }

            Write-Log -Message "Exchange Organization is: $ExOrg"
        }
        else 
        {
            if(![System.String]::IsNullOrEmpty($Config.Organization)) 
            {
                Write-Log -Message "Exchange Organization will be: $($Config.Organization)."
            }
            else 
            {
                $Msg = "Organization not specified and no Exchange Organization discovered."
                throw $Msg
            }
        }

        Write-Log -Message "Checking Exchange Forest Schema Version"
                
        if($MajorSetupVersion -ge 15.01) {
            $MinForestLevel = $EX2016_MINFORESTLEVEL
            $MinDomainLevel = $EX2016_MINDOMAINLEVEL
        }
        else {
            $MinForestLevel = $EX2013_MINFORESTLEVEL
            $MinDomainLevel = $EX2013_MINDOMAINLEVEL
        }
        
        $ExchangeForestLevel = Get-ExchangeForestLevel

        if ($ExchangeForestLevel -ne $null) 
        {
            Write-Log -Message "Exchange Forest Schema Version is $ExchangeForestLevel."

            if ($Config.Phase -eq 4 -and $ExchangeForestLevel -lt $MinForestLevel) 
            {
                # Only check before starting setup
                $Msg = "Minimum required Forest Functional Level version is $MinForestLevel, aborting."
                throw $Msg
            }
        }
        else 
        {
            Write-Log -Message "Active Directory is not prepared" -Level WARNING
        }

        Write-Log -Message "Checking Exchange Domain Version"
        $ExchangeDomainLevel = Get-ExchangeDomainLevel
        
        if($ExchangeDomainLevel -ne $null) 
        {
            Write-Log -Message "Exchange Domain Version is $ExchangeDomainLevel."

            if ($Config.Phase -eq 4 -and $ExchangeDomainLevel -lt $MinDomainLevel) 
            {
                # Only check before starting setup
                $Msg = "Minimum required Domain Functional Level version is $MinDomainLevel, aborting."
                throw $Msg
            }
        }

        Write-Log -Message "Checking domain mode"

        if ((Test-DomainNativeMode) -eq $false) 
        {
            $Msg = "Domain is in mixed mode, native mode is required"
            throw $Msg
        }
        else 
        {
            Write-Log -Message "Domain is in native mode"
        }

        Write-Log -Message "Checking Forest Functional Level"
        if ((Get-ForestFunctionalLevel) -lt $FOREST_LEVEL2003) 
        {
            $Msg = "Forest is not Functional Level 2003 or later"
            throw $Msg
        }
        else 
        {
            Write-Log -Message "Forest Functional Level is 2003 or later"
        }

        if ((Get-PSExecutionPolicy) -ne $null) 
        {
            # Referring to http://support.microsoft.com/kb/2810617/en
            Write-Log -Message "PowerShell Execution Policy is configured through GPO and may prohibit Exchange Setup. Clearing entry." -Level ERROR
            Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell" -Name ExecutionPolicy -Value "" -Force
        }

        if ($Config.Unattended) 
        {           
            Write-Log -Message "Checking provided credentials"
            
            if ($Credential -ne [PSCredential]::Empty) 
            {
                $Result = Test-Credentials -Credential $Credential
            }
            else 
            {
                Write-Log -Message "Unattended specified, but no credentials provided." -Level ERROR
                $Result = $false
            }

            if ($Result -ne $true) 
            {
                $Msg = "Provided credentials don't seem to be valid."
                throw $Msg
            } 
        }

        Write-Output -InputObject $true
    }

    End {        
    }
}

Function Start-ExchangeADPrep {
    <#
        .SYNOPSIS
            Runs the Active Directory preparation for Exchange.
 
        .DESCRIPTION
            This cmdlet tests and then prepares Active Directory using the standard Exchange installer.
 
        .PARAMETER Organization
            The Exchange Organization name that is being installed.
 
        .PARAMETER SetupFilePath
            The path to the setup.exe file used to install Exchange.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-ExchangeADPrep -Organization "contoso" -SetupFilePath "c:\exchangefiles\setup.exe"
 
            Runs the Exchange AD prep.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.String]$Organization,

        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]$SetupFilePath
    )

    Begin {}

    Process {
        Write-Log -Message "Preparing Active Directory."
        $Params = @()

        Write-Log -Message "Checking Exchange Organization existence."

        if ((Test-ExchangeOrganization -Organization $Organization) -eq $false) {
            $Params += "/PrepareAD"
            $Params += "/OrganizationName:`"$Organization`""
        }
        else {
            Write-Log -Message "Organization $Organization exists, checking Exchange Forest Schema and Domain versions."
            $ForestLevel = Get-ExchangeForestLevel
            $DomainLevel = Get-ExchangeDomainLevel

            Write-Log -Message "Exchange Forest Schema version: $ForestLevel, Domain: $DomainLevel."

            $Version = New-Object -TypeName System.IO.FileInfo("$SetupFilePath") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
            $MajorSetupVersion = [System.Decimal]"$($Version.Split(".")[0]).$($Version.Split(".")[1])"

            if ($MajorSetupVersion -ge 15.01) {
                $MinForestLevel = $EX2016_MINFORESTLEVEL
                $MinDomainLevel = $EX2016_MINDOMAINLEVEL
            }
            else {
                $MinForestLevel = $EX2013_MINFORESTLEVEL
                $MinDomainLevel = $EX2013_MINDOMAINLEVEL
            }

            if ($ForestLevel -lt $MinForestLevel -or $DomainLevel -lt $MinDomainLevel) {
                Write-Log -Message "Exchange Forest Schema or Domain needs updating. Required: Forest($MinForestLevel) / Domain($MinDomainLevel)." -Level WARNING
                $Params += "/PrepareAD"
            }
            else {
                Write-Log -Message "Active Directory is up to date."
            }
        }

        if ($Params.Count -gt 0) 
        {
            Write-Log -Message "Preparing Active Directory, Exchange Organization is $Organization."
            $Params += "/IAcceptExchangeServerLicenseTerms"
            Start-ProcessWait -FilePath $SetupFilePath -ArgumentList $Params -EnableLogging

            if (!(Test-ExchangeOrganization -Organization $Organization) -or (Get-ExchangeForestLevel) -lt $MinForestLevel -or (Get-ExchangeDomainLevel) -lt $MinDomainLevel) 
            {
                $Msg = "Problem updating schema, domain, or Exchange organization."
                throw $Msg
            }
            else 
            {
                Write-Log -Message "Active Directory has been successfully prepared for Exchange."
            }
        }
        else 
        {
            Write-Log -Message "Exchange organization $Organization already exists, skipping this step."
        }
    }

    End {

    }
}

Function Start-ExchangeInstallation {
    <#
        .SYNOPSIS
            Initiates the installation of a new Exchange environment.
 
        .DESCRIPTION
            This cmdlet runs the installation of Exchange using the setup.exe Exchange installer.
 
        .PARAMETER InstallMailbox
            Specify to install the mailbox role.
 
        .PARAMETER InstallCAS
            Specify to install the CAS role, this is ignored for Exchange 2016.
 
        .PARAMETER MDBName
            The name of the database file to be created, do not include an extension.
 
        .PARAMETER MDBDBPath
            The folder location to store the database file. The MDBName parameter is required to use this parameter.
 
        .PARAMETER MDBLogPath
            The folder location to store the database log files.
 
        .PARAMETER TargetDirectory
            The target directory for installation. This will default to the Exchange default.
 
        .PARAMETER SetupFilePath
            The path to the Exchange setup.exe file.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-ExchangeInstallation -InstallMailbox `
                                         -MDBName MyDB `
                                         -MDBDBPath "c:\exchange\db" `
                                         -MDBLogPath "c:\exchange\logs" `
                                         -SetupFilePath "c:\exchangefiles\setup.exe"
 
            Runs the exchange installation.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>
  
    [CmdletBinding()]
    Param(
        [Parameter()]
        [switch]$InstallMailbox,

        [Parameter()]
        [switch]$InstallCAS,

        [Parameter()]
        [System.String]$MDBName,

        [Parameter()]
        [System.String]$MDBDBPath,

        [Parameter()]
        [System.String]$MDBLogPath,

        [Parameter()]
        [System.String]$TargetDirectory,

        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]$SetupFilePath
    )

    Begin {}

    Process {
        $Version = New-Object -TypeName System.IO.FileInfo("$SetupFilePath") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
        $MajorSetupVersion = [System.Decimal]"$($Version.Split(".")[0]).$($Version.Split(".")[1])"
        Write-Log -Message "Installing Microsoft Exchange Server ($Version)."

        if ($MajorSetupVersion -ge 15.01) {
            $PresenceKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{CD981244-E9B8-405A-9026-6AEB9DCEF1F1}"
        }
        else {
            $PresenceKey= "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{4934D1EA-BE46-48B1-8847-F1AF20E892C1}"
        }

        $Roles = @()

        if ($InstallMailbox) {
            $Roles += "Mailbox"
        }

        if ($InstallCAS) {
            if ($MajorSetupVersion -ge 15.01) {
                Write-Log -Message "Ignoring specified InstallCAS option for Exchange 2016." -Level WARNING
            }
            else {
                $Roles += "ClientAccess"
            }
        }

        $Roles = $Roles -join ","

        $Params = @("/Mode:Install", "/Roles:`"$Roles`"", "/IAcceptExchangeServerLicenseTerms","/InstallWindowsComponents")

        if ($InstallMailbox) {
            if (![System.String]::IsNullOrEmpty($MDBName)) {
                $Params += "/MdbName:`"$MDBName`""

                if (![System.String]::IsNullOrEmpty($MDBDBPath)) {
                    $Params += "/DBFilePath:`"$MDBDBPath\$MDBName.edb`""
                }
            }

            if (![System.String]::IsNullOrEmpty($MDBLogPath)) {
                $Params += "/LogFolderPath:`"$MDBLogPath`""
            }
        }

        if (![System.String]::IsNullOrEmpty($TargetDirectory)) {
            $Params += "/TargetDir:`"$TargetDirectory`""
        }

        $Params += "/DoNotStartTransport"

        Start-ProcessWait -FilePath $SetupFilePath -ArgumentList $Params -EnableLogging

        if ((Get-Item -Path $PresenceKey -ErrorAction SilentlyContinue) -eq $null) 
        {
            $Msg = "Error encountered installing Exchange"
            throw $Msg
        }
        else 
        {
            $LocalAdmins = Get-LocalGroupMembers -LocalGroup "Administrators"
            Write-Log -Message "Current local admins: `n$($LocalAdmins -join "`n")" -Level VERBOSE
        }
    }

    End {
    }
}

Function Install-Exchange {
    <#
        .SYNOPSIS
            Runs the complete testing, preparation, installation, and cleanup for an Exchange installation.
 
        .DESCRIPTION
            This cmdlet performs all steps necessary to install Exhange 2013/2016. The installation runs in phases with a reboot after each phase.
 
            If the installation is being run unattended, scheduled tasks are used to continue the installation process. A single configuration file is generated from the parameters that is used
            to persist the config and also identify which phase is being executed by this cmdlet.
 
        .PARAMETER Organization
            The Exchange Organization name that is being installed.
 
        .PARAMETER InstallMailbox
            Specify to install the mailbox role.
 
        .PARAMETER InstallCAS
            Specify to install the CAS role, this is ignored for Exchange 2016.
 
        .PARAMETER MDBName
            The name of the database file to be created, do not include an extension.
 
        .PARAMETER MDBDBPath
            The folder location to store the database file. The MDBName parameter is required to use this parameter.
 
        .PARAMETER MDBLogPath
            The folder location to store the database log files.
 
        .PARAMETER TargetDirectory
            The target directory for installation. This will default to the Exchange default.
 
        .PARAMETER TempDirectory
            The location to temporarily store downloaded setup files. This defaults to the specified SourceDirectory containing the Exchange setup files.
 
        .PARAMETER Unattended
            Specify that this will be an unattended installation and will perform all necessary reboots and use Windows Task Scheduler to continue installation after reboot.
 
        .PARAMETER UnattendedTaskName
            The name of the scheduled task that will be used to conduct the unattended installation. This defaults to $script:InstallExchangeTaskName which is InstallExchange.
 
        .PARAMETER SourceDirectory
            The path to the folder containing the Exchange setup files. The setup.exe file should be at the root of this directory. The Exchange installation media should already be extracted from the
            ISO or exe in this directory.
 
        .PARAMETER InstallFilterPack
            Specify to install the Office filter pack.
 
        .PARAMETER IncludeFixes
            Specify to install and/or run all applicable KBs or FixIts for this version of Exchange.
 
        .PARAMETER Phase
            Indicate if you wish to start on a phase of the install other than 1. The 6 phases are
 
            1) Install OS Prerequisites
            2) Install Exchange Prequisites
            3) Install UCMA and prepare AD
            4) Install Exchange
            5) Run post configuration tasks
            6) Complete setup actions, add server to DAG, perform cleanup
 
        .PARAMETER NoSetup
            This switch specifies that only the prerequisite steps are performed and no installation is conducted. This is why the PrepareAD step is broken out into a separate phase from the
            Install Exchange phase.
 
        .PARAMETER TargetDirectory
            The directory that Exchange will be installed into. This defaults to the Exchange setup default.
 
        .PARAMETER DAGName
            Specify the name of the DAG if this Exchange server should either setup a new DAG or join a DAG with the specified name. The cmdlet will determine which action to perform.
 
            Leave blank to run a standalone installation.
 
        .PARAMETER ProductKey
            Specify the product key to use if the installation media you are using does not have an embedded license key.
 
        .PARAMETER Credential
            The credential to use to execute an unattended installation and the credential that will be used to modify Active Directory.
 
        .PARAMETER ConfigFilePath
            The path to the existing configuration file. This is used by the unattended setup or can be used to run each phase manually without re-entering parameters.
 
        .PARAMETER RetryCount
            The number of times a phase will be retried if it fails. This defaults to 1 and has a maximum value of 3.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Install-Exchange -Organization "Contoso" `
                                -InstallMailbox `
                                -MDBDBPath "c:\Exchange\DB" `
                                -MDBName "MDB1" `
                                -MDBLogPath "c:\Exchange\Logs" `
                                -Unattended `
                                -SourceDirectory "c:\ExchangeSetup" `
                                -InstallFilterPack `
                                -IncludeFixes `
                                -Credential (Get-Credential)
 
            Launches an unattended Exchange installation for standalone instance.
 
        .EXAMPLE
            Install-Exchange -Organization "Contoso" `
                                -InstallMailbox `
                                -MDBDBPath "c:\Exchange\DB" `
                                -MDBName "MDB1" `
                                -MDBLogPath "c:\Exchange\Logs" `
                                -Unattended `
                                -SourceDirectory "c:\ExchangeSetup" `
                                -InstallFilterPack `
                                -IncludeFixes `
                                -Credential (Get-Credential) `
                                -DAGName "DAG1"
 
            Launches an unattended Exchange installation for a DAG configuration.
 
        .EXAMPLE
            Install-Exchange -Organization "Contoso" `
                                -NoSetup `
                                -Unattended `
                                -SourceDirectory "c:\ExchangeSetup" `
                                -Credential (Get-Credential)
 
            Launches an unattended Exchange prerequisite installation, but does not install Exchange.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/26/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(ParameterSetName="Parameters")]
        [System.String]$Organization,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$InstallMailbox,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$InstallCAS,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$MDBDBPath,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$MDBLogPath,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$MDBName,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$TempDirectory,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$Unattended,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$UnattendedTaskName = $script:InstallExchangeTaskName,

        [Parameter(ParameterSetName="Parameters", Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]$SourceDirectory,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$InstallFilterPack,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$IncludeFixes,

        [Parameter(ParameterSetName="Parameters")]
        [ValidateRange(1,6)]
        [System.Int32]$Phase = 1,

        [Parameter(ParameterSetName="Parameters")]
        [switch]$NoSetup,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$TargetDirectory,

        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                $_.Length -le 15
            }
            else {
                return true
            }
        })]
        [System.String]$DAGName = [System.String]::Empty,

        [Parameter(ParameterSetName="Parameters")]
        [System.String]$ProductKey,

        [Parameter(ParameterSetName="Parameters")]
        [ValidateRange(0, 3)]
        [System.Int32]$RetryCount = 1,

        [Parameter()] 
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]    
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(ParameterSetName="ConfigFile", Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]$ConfigFilePath
    )

    DynamicParam {
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
        $ValidateScript = New-Object -TypeName System.Management.Automation.ValidateScriptAttribute([System.Management.Automation.ScriptBlock]::Create("if (![System.String]::IsNullOrEmpty(`$_)) { Test-Path -Path `$_ } else { return `$true }"))
        
        if (![System.String]::IsNullOrEmpty($DAGName)) {
            [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
            $Attributes.ParameterSetName = "Parameters"
            $Attributes.Mandatory = $false
            $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($Attributes)
            $AttributeCollection.Add($ValidateScript)

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("WitnessServer", [System.String], $AttributeCollection)
            $DynParam.Value = [System.String]::Empty
            $ParamDictionary.Add("WitnessServer", $DynParam)
        }

        return $ParamDictionary  
    }

    Begin {        
    }

    Process {
        $ScriptFullName = $MyInvocation.MyCommand.Path
        $ParameterString = $PSBoundParameters.GetEnumerator() -join " "

        if ([System.String]::IsNullOrEmpty($TempDirectory)) 
        {
            $TempDirectory = $SourceDirectory
        }

        if ([System.String]::IsNullOrEmpty($UnattendedTaskName))
        {
            $UnattendedTaskName = $script:InstallExchangeTaskName
        }

        if ($PSCmdlet.ParameterSetName -ne "ConfigFile") 
        {
            $ConfigFile = "$TempDirectory\InstallExchange_config.json"
        }
        else 
        {
            $ConfigFile = $ConfigFilePath
        }

        Write-Log -Message "Cmdlet called using $ParameterString."
        Write-Log -Message "Running on OS Build $script:MajorOSVersion.$script:MinorOSVersion."
        Write-Log -Message "Logging to $script:LogPath." -Level VERBOSE
        Write-Log -Message "Config file path: $ConfigFile." -Level VERBOSE

        if ($PSCmdlet.ParameterSetName -eq "ConfigFile") 
        {
            $Config = ConvertFrom-Json -InputObject (Get-Content -Path "$ConfigFile" -Raw)
        }
        else 
        {
            #No config file, initialize from parameters

            if ($Unattended -eq $true -and $Credential -eq [PSCredential]::Empty) {
                try 
                {
                    Write-Log -Message "Credentials not specified, prompting..."
                    $Credential = Get-Credential
                }
                catch [Exception] 
                {
                    $Msg = "Unattended setup specified, but no or improper credentials provided."
                    Write-Log -Message $Msg  -Level ERROR
                    throw $Msg
                }
            }

            $Config = @{}

            $Config.InstallMailbox = [bool]$InstallMailbox
            $Config.InstallCAS = [bool]$InstallCAS
            $Config.InstallMDBDBPath = $MDBDBPath
            $Config.InstallMDBLogPath = $MDBLogPath
            $Config.InstallMDBName = $MDBName
            $Config.TempDirectory = $TempDirectory
            $Config.PreviousPhase = ($Phase - 1)
            $Config.Phase = $Phase
            $Config.Organization = $Organization
            $Config.SourceDirectory = $SourceDirectory
            $Config.SetupVersion = Get-FileVersion -Path "$SourceDirectory\setup.exe"
            $Config.TargetDirectory = $TargetDirectory
            $Config.Unattended = [bool]$Unattended
            $Config.IncludeFixes = [bool]$IncludeFixes
            $Config.InstallFilterPack = [bool]$InstallFilterPack
            $Config.NoSetup = [bool]$NoSetup
            $Config.SCP = $SCP
            $Config.Verbose = [int]$VerbosePreference
            $Config.FirstRun = $true
            $Config.DAGName = $DAGName
            $Config.WitnessServer = $PSBoundParameters.WitnessServer
            $Config.ProductKey = $ProductKey
            $Config.TaskName = $UnattendedTaskName
            $Config.MaxRetries = $RetryCount
            $Config.RetryNumber = 0

            if(![System.String]::IsNullOrEmpty($Config.DAGName) -and [System.String]::IsNullOrEmpty($Config.WitnessServer)) {
                Write-Log -Message "No witness server defined, using a domain controller." -Level WARNING
                $Ctx = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain)
                $Server = [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($Ctx) | Select-Object -ExpandProperty Name
                $Config.WitnessServer = $Server
                Write-Log -Message "Selected $Server for the witness server."
            }

            Set-Content -Path "$ConfigFile" -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false 
        }

        if ($Config.Unattended -eq $true) {
            if ($Config.FirstRun -eq $true) {
                $Command = @"
try {
    Set-RunOnceScript -Command "`$env:SystemDrive\MonitorLog.ps1" -RunFile -Name "$script:RunOnceTaskName"
    Get-Content -Path "$script:LogPath" -Wait
}
catch [Exception] {
    Write-Log -Message "Error running get-content for RunOnce command." -Level ERROR -ErrorRecord `$_
}
"@

                Set-Content -Path "$env:SystemDrive\MonitorLog.ps1" -Value $Command -Force -Confirm:$false
            }

            Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile -Name $script:RunOnceTaskName
        }

        if ($Config.NoSetup -eq $true) {
            $MAX_PHASE = 3
        }
        else {
            $MAX_PHASE = 6
        }

        $VerbosePreference = $Config.Verbose

        if ($Config.Unattended -eq $true -and $Config.Phase -gt 1) 
        {
            Write-Log -Message "Will continue unattended installation of Exchange."
        }

        if ($Config.FirstRun -eq $true -or $PSBoundParameters.ContainsKey("Phase")) 
        {
            Write-Log -Message "Enabling Task Scheduler History." -Level VERBOSE
            $LogName = 'Microsoft-Windows-TaskScheduler/Operational'
            $EventLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $LogName
            $EventLog.IsEnabled = $true
            $EventLog.SaveChanges()

            try 
            {
                Write-Log -Message "Performing sanity checks for Exchange readiness on first run." -Level VERBOSE

                Test-ExchangeReadiness -Config $Config -Credential $Credential | Out-Null

                Write-Log -Message "Successfully completed sanity checks" -Level VERBOSE
            }
            catch [Exception]
            {
                Write-Log -ErrorRecord $_ 
                throw $_.Exception
            }
        }
        else 
        {
            Write-Log -Message "The install phase is $($Config.Phase), skipping sanity checks."
        }

        Set-OpenFileSecurityWarning -Disable

        Write-Log -Message "Checking for pending reboot..."

        if (Test-PendingReboots) 
        {
            if ($Config.Unattended) 
            {
                Write-Log -Message "Reboot pending, will reboot the system and rerun this phase, $($Config.Phase)."
            }
            else 
            {
                Write-Log -Message "Reboot pending, please reboot the system and restart the script. The parameters will be saved in the config file at $ConfigFile." -Level WARNING
            }
        }
        else 
        {
            if ($Config.PreviousPhase -eq $Config.Phase -and $Config.RetryNumber -gt $Config.MaxRetries) {
                $Msg = "Caught the installation running in a loop. The previous phase $($Config.PreviousPhase) is the same as the current phase and maximum retries have been reached."
                Write-Log -Message $Msg -Level ERROR
                Write-Log -Message "Removing scheduled task $($Config.TaskName) and ending installation."
                
                if ((Get-ScheduledTask -TaskName "$($Config.TaskName)" -ErrorAction SilentlyContinue) -ne $null) 
                {
                    Unregister-ScheduledTask -TaskName "$($Config.TaskName)" -Confirm:$false
                    Write-Log -Message "Successfully removed scheduled task."
                }
                else 
                {
                    Write-Log -Message "No scheduled task matching $($Config.TaskName) present."
                }

                throw $Msg
            }
            else {
                Write-Log -Message "Current phase is $($Config.Phase) of $MAX_PHASE."
                $Config.PreviousPhase = $Config.Phase
                Set-Content -Path "$ConfigFile" -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false 

                [System.Boolean]$PhaseSuccess = $true

                switch ($Config.Phase) {
                    1 {
                        Write-Log -Message "*** PHASE 1 *** : Installing Operating System prerequisites."
                        $Features = @("Desktop-Experience", "RSAT-ADDS", "RSAT-Clustering-CmdInterface")

                        if ($MajorOSVersion -eq $WS2008R2_MAJOR) 
                        {
                            $Features += "NET-Framework"
                        }
                        else 
                        {
                            $Features += "Server-Media-Foundation"
                        }

                        if (![System.String]::IsNullOrEmpty($Config.DAGName)) 
                        {
                            $Features += "Failover-Clustering"
                        }

                        try 
                        {
                            Import-Module -Name ServerManager -ErrorAction Stop
                            Add-WindowsFeature -Name $Features -ErrorAction Stop | Out-Null

                            foreach ($Feature in $Features) 
                            {
                                if ((Get-WindowsFeature -Name "$Feature" -ErrorAction SilentlyContinue) -eq $null) 
                                {
                                    Write-Log -Message "Feature $Feature appears not to be installed after attempting installation." -Level ERROR
                                    $PhaseSuccess = $false
                                }
                            }
                        }
                        catch [Exception] 
                        {
                            $PhaseSuccess = $false
                            Write-Log -Message "Error installing windows features." -Level ERROR -ErrorRecord $_                            
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            Write-Log -Message "Completed Operating System prerequisites."
                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    2 {
                        Write-Log -Message "*** PHASE 2 *** : Installing Exchange prerequisites."

                        try
                        {
                            if ($Config.InstallFilterPack -eq $true) 
                            {
                                foreach ($Item in $script:FilterPacks) 
                                {
                                    [System.Uri]$Uri = New-Object -TypeName System.Uri("$($Item.Url)")
                                    $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                    Start-PackageInstallation -PackageId "$($Item.PackageId)" -PackageName "$($Item.PackageName)" -Url "$($Item.Url)" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Item.Arguments
                                }
                            }

                            #Check if .NET 4.5.2 or later installed
                            $NETVersion = Get-NETVersion
                            if ($NETVersion -lt $script:NET452) 
                            {
                                if ($Config.SetupVersion -ge $EX2013STOREEXE_CU7) 
                                {
                                    if ($MajorOSVersion -eq $WS2008R2_MAJOR) 
                                    {
                                        $PackageId = "{26784146-6E05-3FF9-9335-786C7C0FB5BE}"
                                    }
                                    else {
                                        $PackageId = "KB2934520"
                                    }

                                    $Url = "http://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe"
                                    [System.Uri]$Uri = New-Object -TypeName System.Uri($Url)
                                    $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                    Start-PackageInstallation -PackageId $PackageId -PackageName "Microsoft .NET Framework 4.5.2" -Url $Url -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/q", "/norestart")
                                }
                            }
                            else 
                            {
                                Write-Log -Message ".NET Framework 4.5.2 or later already installed."
                            }

                            $Minimum2013Version = [System.Decimal]$EX2013STOREEXE_CU13.Replace(".","").Insert(4, ".")
                            $Minimum2016Version = [System.Decimal]$EX2016STOREEXE_CU2.Replace(".","").Insert(4, ".")
                            $CurrentVersion = [System.Decimal]$Config.SetupVersion.Replace(".","").Insert(4, ".")

                            Write-Log -Message "Current Version $CurrentVersion, Minimum 2013 Version for .NET 4.6.1 $Minimum2013Version, Minimum 2016 Version for .NET 4.6.1 $Minimum2016Version."

                            if ((($CurrentVersion -lt $Minimum2016Version) -and ($CurrentVersion -ge 1501)) -or (($CurrentVersion -lt $Minimum2013Version) -and ($CurrentVersion -lt 1501))) 
                            {
                                Write-Log -Message "Blocking .NET 4.6.1 installation for all installations below Exchange 2016 CU2 or Exchange 2013 CU13."
                                Set-NET461InstallBlock
                            }
                            else 
                            {
                                if ($NETVersion -ge $script:NET46) 
                                {
                                    Write-Log -Message "Installing Exchange 2016 CU2 or Exchange 2013 CU13 or greater and at least .NET 4.6 installed, installing .NET 4.6.1 hotfix rollups."

                                    switch ($MajorOSVersion) {
                                        $WS2016_MAJOR {
                                            $Url = ""
                                            $PackageId = ""
                                            $Arguments = @()
                                            break
                                        }
                                        $WS2012R2_MAJOR {
                                            $Url = "http://download.microsoft.com/download/6/8/0/680ee424-358c-4fdf-a0de-b45dee07b711/windows8.1-kb3154528-x64.msu"
                                            $PackageId = "KB3154528"
                                            $Arguments = @("/install", "/quiet", "/norestart")
                                            break
                                        }
                                        $WS2012_MAJOR {
                                            $Url = "http://download.microsoft.com/download/6/8/0/680ee424-358c-4fdf-a0de-b45dee07b711/windows8-rt-kb3154527-x64.msu"
                                            $PackageId = "KB3154527"
                                            $Arguments = @("/install", "/quiet", "/norestart")
                                            break
                                        }
                                        $WS2008R2_MAJOR {
                                            $Url = "http://download.microsoft.com/download/6/8/0/680ee424-358c-4fdf-a0de-b45dee07b711/ndp461-kb3154529-x86-x64-enu.exe"
                                            $PackageId = "KB3154529"
                                            $Arguments =  @("/q", "/norestart")
                                            break
                                        }
                                        default {
                                            Write-Log -Message "Unknown OS version $MajorOSVersion." -Level ERROR
                                            $Url = [System.String]::Empty
                                            $PhaseSuccess = $false
                                            break
                                        }
                                    }

                                    if (![System.String]::IsNullOrEmpty($Url)) 
                                    {
                                        [System.Uri]$Uri = New-Object -TypeName System.Uri($Url)
                                        $FileName = $Uri.Segments[$Uri.Segments.Count - 1]

                                        try 
                                        {
                                            Start-PackageInstallation -PackageId $PackageId -Url $Url -PackageName "Hotfix rollup for the .NET Framework 4.6 and 4.6.1 in Windows" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Arguments
                                        }
                                        catch [Exception] 
                                        {
                                            Write-Log -Message "Error installing .NET 4.6 and 4.6.1 hotfix rollup." -Level ERROR -ErrorRecord $_
                                            $PhaseSuccess = $false
                                        }
                                    }
                                }
                            }
                    
                            if ($PSVersionTable.PSVersion.Major -lt 5) {
                                Write-Log -Message "WMF 5 is not installed, installing now."

                                switch ($MajorOSVersion) {
                                    $WS2016_MAJOR {
                                        $Url = [System.String]::Empty
                                        $PackageId = [System.String]::Empty
                                        break
                                    }
                                    $WS2012R2_MAJOR {
                                        $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/Win8.1AndW2K12R2-KB3134758-x64.msu"
                                        $PackageId = "KB3134758"
                                        break
                                    }
                                    $WS2012_MAJOR {
                                        $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/W2K12-KB3134759-x64.msu"
                                        $PackageId = "KB3134759"
                                        break
                                    }
                                    $WS2008R2_MAJOR {
                                        $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/Win7AndW2K8R2-KB3134760-x64.msu"
                                        $PackageId = "KB3134760"
                                        break
                                    }
                                    default {
                                        Write-Log -Message "Cannot match current Major OS Version for WMF installation." -Level ERROR
                                        $Url = [System.String]::Empty
                                        $PhaseSuccess = $false
                                        break
                                    }
                                }

                                if (![System.String]::IsNullOrEmpty($Url)) 
                                {
                                    [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url")
                                    $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                    Start-PackageInstallation -PackageId $PackageId -PackageName "Windows Management Framework 5.0" -Url $Url -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/install", "/quiet", "/norestart")
                                }
                            }
                            else 
                            {
                                Write-Log -Message "PowerShell version $($PSVersionTable.PSVersion.Major) detected."
                            }

                            switch ($MajorOSVersion) {
                                $WS2016_MAJOR {
                                    $PrereqPackages = $script:WS2016Prereqs
                                    break
                                }
                                $WS2012R2_MAJOR {
                                    $PrereqPackages = $script:WS2012R2Prereqs
                                    break
                                }
                                $WS2012_MAJOR {
                                    $PrereqPackages = $script:WS2012Prereqs
                                    break
                                }
                                $WS2008R2_MAJOR {
                                    $PrereqPackages = $script:WS2008R2Prereqs
                                    break
                                }
                                default {
                                    Write-Log -Message "Cannot match current Major OS Version for prereq installation." -Level ERROR
                                    $PrereqPackages = @()
                                    $PhaseSuccess = $false
                                    break
                                }
                            }

                            foreach ($Item in $PrereqPackages) 
                            {
                                [System.Uri]$Uri = New-Object -TypeName System.Uri("$($Item.Url)")
                                $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                Start-PackageInstallation -PackageId "$($Item.PackageId)" -PackageName "$($Item.PackageName)" -Url "$($Item.Url)" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Item.Arguments
                            }

                        }
                        catch [Exception]
                        {
                            $PhaseSuccess = $false
                            Write-Log -ErrorRecord $_ -Level ERROR
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            Write-Log -Message "Completed Operating System prerequisites."

                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    3 {
                        Write-Log -Message "*** PHASE 3 *** : Installing Exchange prerequisites (continued)."

                        try
                        {
                            $Url = "http://download.microsoft.com/download/2/C/4/2C47A5C1-A1F3-4843-B9FE-84C0032C61EC/UcmaRuntimeSetup.exe"
                            [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url")
                            $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                            Start-PackageInstallation -PackageId "{41D635FE-4F9D-47F7-8230-9B29D6D42D31}" -PackageName "Unified Communications Managed API 4.0 Runtime" -Url "$Url" -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/q", "/norestart")

                            if (![System.String]::IsNullOrEmpty($Config.Organization)) 
                            {
                                Write-Log -Message "Checking/Preparing Active Directory."
                                Start-ExchangeADPrep -Organization "$($Config.Organization)" -SetupFilePath "$($Config.SourceDirectory)\setup.exe"
                            }

                            Write-Log -Message "Completed installing Exchange prerequisites."
                        }
                        catch [Exception]
                        {
                            Write-Log -ErrorRecord $_ -Level ERROR
                            $PhaseSuccess = $false
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    4 {
                        Write-Log -Message "*** PHASE 4 *** : Installing Exchange."

                        try
                        {
                            $MajorSetupVersion = "$($Config.SetupVersion.Split(".")[0]).$($Config.SetupVersion.Split(".")[1])"

                            Start-ExchangeInstallation -InstallMailbox:$Config.InstallMailbox `
                                                        -InstallCAS:$Config.InstallCAS `
                                                        -MDBName $Config.InstallMDBName `
                                                        -MDBDBPath $Config.InstallMDBDBPath `
                                                        -MDBLogPath $Config.InstallMDBLogPath `
                                                        -TargetDirectory "$($Config.TargetDirectory)" `
                                                        -SetupFilePath "$($Config.SourceDirectory)\setup.exe"
                                                                     
                            if (Get-Service -Name MSExchangeTransport -ErrorAction SilentlyContinue) {
                                Write-Log -Message "Configuring MSExchangeTransport startup to Manual."
                                Set-Service -Name MSExchangeTransport -StartupType Manual
                            }

                            if(Get-Service -Name MSExchangeFrontEndTransport -ErrorAction SilentlyContinue) {
                                Write-Log -Message "Configuring MSExchangeFrontEndTransport startup to Manual."
                                Set-Service -Name MSExchangeFrontEndTransport -StartupType Manual
                            }

                            switch($Config.SCP) {
                                "" {
                                    # Do nothing
                                    break
                                }
                                $null   {
                                    Write-Log -Message "Removing Service Connection Point record"
                                    Remove-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME
                                    break
                                }
                                default {
                                    Write-Log -Message "Configuring Service Connection Point record as $($Config.SCP)"
                                    Add-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME -ServiceBinding $Config.SCP
                                    break
                                }
                            }

                            Write-Log -Message "Completed Exchange installation step."
                        }
                        catch [Exception]
                        {
                            Write-Log -ErrorRecord $_ -Level ERROR
                            $PhaseSuccess = $false
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    5 {
                        Write-Log -Message "*** PHASE 5 *** : Post configuration tasks."

                        try
                        {
                            Set-HighPerformancePowerPlan
                            Set-Pagefile
                            Disable-SSLv3                        

                            if ($Config.InstallMailbox) 
                            {
                                if ($Config.InstallFilterPack) 
                                {
                                    Write-Log -Message "Enabling IFilters."
                                    Enable-IFilters
                                }
                                # Insert other Mailbox Server specifics here
                            }
             
                            if($Config.InstallCAS) 
                            {
                                # Insert Client Access Server specifics here
                            }

                            if($Config.IncludeFixes) {
                                Write-Log -Message "Installing applicable recommended hotfixes and security updates."
                                $Version = Get-FileVersion -ServiceName "MSExchangeServiceHost" 
                                Write-Log -Message "Installed Exchange MSExchangeIS version is $(Get-TextVersion -FileVersion $Version)" -Level VERBOSE

                                switch($Version) {
                                    $EX2013STOREEXE_CU2 {
                                        $Url = "http://download.microsoft.com/download/3/D/A/3DA5AC0D-4B94-479E-957F-C7C66DE1B30F/Exchange2013-KB2880833-x64-en.msp"
                                        [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url")
                                        $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                        Start-PackageInstallation -PackageId "KB2880833" -PackageName "Security Update For Exchange Server 2013 CU2" -Destination "$($Config.SourceDirectory)\$FileName" -Url $Url -Arguments @("/q", "/norestart")
                                        break
                                    }
                                    $EX2013STOREEXE_CU3 {                
                                        $Url = "http://download.microsoft.com/download/0/E/3/0E3FFD83-FE6A-48B7-85F2-3EF92155EFBE/Exchange2013-KB2880833-x64-en.msp"
                                        [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url")
                                        $FileName = $Uri.Segments[$Uri.Segments.Count - 1]
                                        Start-PackageInstallation -PackageId "KB2880833" -PackageName "Security Update For Exchange Server 2013 CU3" -Destination "$($Config.SourceDirectory)\$FileName" -Url $Url -Arguments @("/q", "/norestart")
                                        break
                                    }
                                    $EX2013STOREEXE_SP1 {
                                        Start-ExchangeFixIt -KB KB2938053
                                        break
                                    }
                                    $EX2013STOREEXE_CU5 {
                                        Set-DisableSharedCacheServiceProbe
                                        break
                                    }
                                    $EX2013STOREEXE_CU6 {
                                        Start-ExchangeFixIt -KB KB2997355
                                        break
                                    }
                                    default {
                                        Write-Log -Message "No updates to install for Exchange."
                                        break
                                    }
                                }
                            }

                            if (![System.String]::IsNullOrEmpty($Config.ProductKey)) {
                                Write-Log -Message "Setting product key $($Config.ProductKey)."
                                Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn
                                Get-ExchangeServer | Where-Object {$_.IsE15OrLater} | ForEach-Object {
                                    Set-ExchangeServer -Identity $_ -ProductKey $Config.ProductKey
                                }
                                Write-Log -Message "Successfully set the prodcut key, restarting the Information Store service."
                                Restart-Service -Name MSExchangeIS -Force -Confirm:$false
                            }

                            if (![System.String]::IsNullOrEmpty($Config.DAGName) -and ![System.String]::IsNullOrEmpty($Config.WitnessServer)) {
                                Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn

                                if ((Get-DatabaseAvailabilityGroup -Identity $Config.DAGName -ErrorAction SilentlyContinue) -eq $null) {
                                    Write-Log -Message "Adding Exchange Trusted Subsystem to Local Administrators on $($Config.WitnessServer) in preparation for DAG creation."
                                    
                                    $Success = Add-DomainMemberToLocalGroup -LocalGroup "Administrators" -Member "Exchange Trusted Subsystem" -MemberType Group -ComputerName $Config.WitnessServer

                                    if (-not $Success)
                                    {
                                        Write-Log -Message "Could not add the Exchange Trusted Subsystem to the witness server local administrators group. Cannot complete DAG setup." -Level ERROR
                                        $PhaseSuccess = $false
                                    }
                                }
                            }

                            Write-Log -Message "Completed post-configuration tasks."
                        }
                        catch [Exception]
                        {
                            $PhaseSuccess = $false
                            Write-Log -ErrorRecord $_ -Level ERROR
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    6 {
                        Write-Log -Message "*** PHASE 6 *** Completing setup actions."

                        try
                        {
                            if ((Get-Service -Name MSExchangeTransport -ErrorAction SilentlyContinue) -ne $null) 
                            {
                                Write-Log -Message "Configuring MSExchangeTransport startup to Automatic."
                                Set-Service MSExchangeTransport -StartupType Automatic

                                try 
                                {
                                    Start-Service -Name MSExchangeTransport
                                }
                                catch [Exception] 
                                {
                                    $PhaseSuccess = $false
                                    Write-Log -Message "Error starting MSExchangeTransport." -Level ERROR -ErrorRecord $_
                                }
                            }

                            if ((Get-Service -Name MSExchangeFrontEndTransport -ErrorAction SilentlyContinue) -ne $null)
                            {
                                Write-Log -Message "Configuring MSExchangeFrontEndTransport startup to Automatic."
                                Set-Service MSExchangeFrontEndTransport -StartupType Automatic

                                try 
                                {
                                    Start-Service -Name MSExchangeFrontEndTransport
                                }
                                catch [Exception] 
                                {
                                    $PhaseSuccess = $false
                                    Write-Log -Message "Error starting MSExchangeFrontEndTransport." -Level ERROR -ErrorRecord $_
                                }
                            }

                            Set-UAC -Enabled $true
                            Set-IEESC -Enabled $true

                            if (![System.String]::IsNullOrEmpty($Config.DAGName) -and ![System.String]::IsNullOrEmpty($Config.WitnessServer)) 
                            {
                                try 
                                {
                                    Write-Log -Message "Adding this server to DAG $($Config.DAGName)."
                                    Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn

                                    $Exists = $false

                                    if ((Get-DatabaseAvailabilityGroup -Identity $Config.DAGName -ErrorAction SilentlyContinue) -eq $null) 
                                    {
                                        Write-Log -Message "Creating a DAG with name $($Config.DAGName)."
                                        New-DatabaseAvailabilityGroup -Name $Config.DAGName -WitnessServer $Config.WitnessServer -DatabaseAvailabilityGroupIPAddress ([System.Net.IPAddress]::None)
                                        Write-Log -Message "Successfully created DAG."
                                    }
                                    else 
                                    {
                                        $Exists = $true
                                    }

                                    try 
                                    {
                                        Write-Log -Message "Adding server to DAG."
                                        Write-Log -Message "Running a gpupdate."
                                        & gpupdate.exe /force

                                        Write-Log -Message "Adding Exchange Trusted Subsystem to Local Administrators in preparation for DAG join."
                                    
                                        $Success = Add-DomainMemberToLocalGroup -LocalGroup "Administrators" -Member "Exchange Trusted Subsystem" -MemberType Group

                                        if ($Success)
                                        {                    
                                            Add-DatabaseAvailabilityGroupServer -Identity $Config.DAGName -MailboxServer $ENV:COMPUTERNAME -ErrorAction Stop
                                            Write-Log -Message "Successfully added server to DAG."

                                            if ($Exists -eq $true) 
                                            {
                                                Write-Log -Message "Since the DAG already exists, go ahead and setup copies of all the available databases."
                                                Get-MailboxDatabase | Where-Object {$_.MasterServerOrAvailabilityGroup -eq $Config.DAGName -and $_.Servers -notcontains $ENV:COMPUTERNAME} | ForEach-Object {
                                                    try 
                                                    {
                                                        Write-Log -Message "Creating a copy of $($_.Name) on local computer $env:COMPUTERNAME."
                                                        Add-MailboxDatabaseCopy -Identity $_.Name -MailboxServer $env:COMPUTERNAME
                                                    }
                                                    catch [Exception] 
                                                    {
                                                        Write-Log -Message "Error creating database copy." -Level ERROR -ErrorRecord $_
                                                    }
                                                }

                                                Write-Log -Message "Setting up database copies on the other servers."

                                                Get-DatabaseAvailabilityGroup -Identity $Config.DAGName | Select-Object -ExpandProperty Servers | Where-Object {$_ -notcontains $ENV:COMPUTERNAME} | ForEach-Object {
                                                    Write-Log -Message "Setting up database copies on $_."
                                                    $Server = $_
                                                    Get-MailboxDatabase | Where-Object {$_.MasterServerOrAvailabilityGroup -eq $Config.DAGName -and $_.Servers -notcontains $Server} | ForEach-Object {
                                                        try 
                                                        {
                                                            Write-Log -Message "Creating a copy of $($_.Name) on remote computer $Server."
                                                            Add-MailboxDatabaseCopy -Identity $_.Name -MailboxServer $Server
                                                        }
                                                        catch [Exception] 
                                                        {
                                                            Write-Log -Message "Error creating database copy." -Level ERROR -ErrorRecord $_
                                                        }
                                                    }
                                                }

                                                Write-Log -Message (Get-MailboxDatabaseCopyStatus | Format-List | Out-String)
                                            }
                                        }
                                        else
                                        {
                                            $PhaseSuccess = $false
                                            Write-Log -Message "The Exchange Trusted Subsystem could not be added to the local administrators group." -Level ERROR                                            
                                        }
                                    }
                                    catch [Exception] 
                                    {
                                        $PhaseSuccess = $false
                                        Write-Log -Message "Error adding server to DAG." -Level ERROR -ErrorRecord $_
                                    }
                                }
                                catch [Exception] 
                                {
                                    $PhaseSuccess = $false
                                    Write-Log -Message "Error creating DAG $($Config.DAGName)." -Level ERROR -ErrorRecord $_
                                }
                            }
                        }
                        catch [Exception]
                        {
                            $PhaseSuccess = $false
                            Write-Log -ErrorRecord $_ -Level ERROR
                        }

                        if ($PhaseSuccess -eq $true)
                        {
                            Write-Log -Message "Setup finished."
                            $Config.Phase++
                            $Config.RetryCount = 0
                        }
                        else
                        {                            
                            $Config.RetryCount++
                        }

                        break
                    }
                    default {
                        Write-Log -Message "Unknown phase $($Config.Phase)." -Level ERROR
                        $PhaseSuccess = $false
                        break
                    }
                }
                #End of switch statement for different phases

                Set-OpenFileSecurityWarning -Enable 

                if ($Config.Unattended -eq $true ) 
                {
                    if ($Config.FirstRun -eq $true) 
                    {                                
                        $Task = New-InstallExchangeScheduledTask -Credential $Credential -ConfigFilePath $ConfigFile -TaskName "$($Config.TaskName)"
                        $Config.FirstRun = $false
                    }

                    #Use less than or equal since the phase is incremented before this check
                    if ($Config.Phase -le $MAX_PHASE) 
                    {
                        Write-Log -Message "Preparing system for the next phase." -Level VERBOSE
                        Set-UAC -Enabled $false
                        Set-IEESC -Enabled $false

                        try 
                        {
                            Write-Log -Message "Saving updated configuration file to $ConfigFile."
                            Set-Content -Path $ConfigFile -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false 
                        }
                        catch [Exception] 
                        {
                            Write-Log -Message "Error saving configuration file." -Level ERROR -ErrorRecord $_
                            throw $_.Exception
                        }

                        if ($PhaseSuccess -eq $false)
                        {
                            if ($Config.RetryCount -le $Config.MaxRetries)
                            {
                                Write-Log -Message "The current phase $($Config.Phase) failed, will retry this phase."
                                Write-Log -Message "Rebooting in $COUNTDOWN_TIMER seconds..."
                                Start-Sleep -Seconds $COUNTDOWN_TIMER
                                Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile -Name $script:RunOnceTaskName
                                Restart-Computer -Force
                            }
                            else
                            {
                                $Msg = "The current phase $($Config.Phase) failed and has reached the maximum number of retries."
                                Write-Log -Message $Msg -Level ERROR

                                Write-Log -Message "Performing cleanup" -Level VERBOSE
                                Start-ExchangeCleanup

                                throw $Msg
                            }
                        }
                        else
                        {                    
                            Write-Log -Message "Rebooting in $COUNTDOWN_TIMER seconds..."
                            Start-Sleep -Seconds $COUNTDOWN_TIMER
                            Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile -Name $script:RunOnceTaskName
                            Restart-Computer -Force
                        }
                    }
                    else 
                    {
                        $Paths = @("$($Config.TempDirectory)", "$env:SystemDrive\MonitorLog.ps1")

                        if ($Config.NoSetup -eq $false) 
                        {
                            $Paths += "$($Config.SourceDirectory)"
                        }

                        Start-ExchangeCleanup -Paths ($Paths | Select-Object -Unique)
                        Write-Log -Message "Unattended setup complete."
                    }
                }
                else
                {
                    Write-Log -Message "Current phase complete, please manually initiate the next phase."
                }
            }
        }
    }

    End {        
    }
}

#region Scheduled Tasks

Function New-InstallExchangeScheduledTask {
    <#
        .SYNOPSIS
            Creates the scheduled task that is used for unattended Exchange installations.
 
        .DESCRIPTION
            This cmdlet creates a scheduled task that will run under the context of the provided credentials.
 
        .PARAMETER Credential
            The credential that the scheduled task will use to run.
 
        .PARAMETER ConfigFilePath
            The path to the configuration file that the scheduled task will use to continue running the Exchange installation.
 
        .PARAMETER TaskName
            The name to use for the scheduled task. This defaults to $script:InstallExchangeTaskName which is InstallExchange.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            New-InstallExchangeScheduledTask -Credential (Get-Credential) -ConfigFilePath "c:\exchangesource\config.json" -TaskName InstallExchange
 
            Creates the scheduled task for the unattended Exchange installation.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/26/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)] 
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]    
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory=$true)]
        [System.String]$ConfigFilePath,

        [Parameter()]
        [System.String]$TaskName = $script:InstallExchangeTaskName
    )

    Begin {        
    }

    Process {
        if ([System.String]::IsNullOrEmpty($TaskName)) 
        {
            $TaskName = $script:InstallExchangeTaskName
        }

        if ((Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) -ne $null) 
        {
            Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
        }

        $Command = "try {Install-Exchange -ConfigFilePath `"$ConfigFilePath`"} catch [Exception] {Write-Log -Message `"Error running Install-Exchange from scheduled task.`" -ErrorRecord `$_ -Level ERROR}"
        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
        $EncodedCommand = [Convert]::ToBase64String($Bytes)
        
        $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand"
        $STSource =  "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe"
        $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams
        $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew

        $ScheduledTask = Register-ScheduledTask -TaskName $TaskName `
                                                -Action $STAction `
                                                -User "$($Credential.UserName)" `
                                                -Password (Convert-SecureStringToString -SecureString $Credential.Password) `
                                                -Trigger (New-ScheduledTaskTrigger -AtStartup -RandomDelay ([System.Timespan]::FromSeconds(30))) `
                                                -Settings $STSettings `
                                                -ErrorAction Stop `
                                                -RunLevel Highest 
    
        Write-Output -InputObject $ScheduledTask                                            
    }

    End {        
    }
}

#endregion