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-WmiObject Win32_OperatingSystem | Select-Object -Property @{Name = "Major"; Expression = {$_.Version.Split(".")[0] + "." +$_.Version.Split(".")[1]}} | Select-Object -ExpandProperty Major
$script:MinorOSVersion = Get-WmiObject Win32_OperatingSystem | Select-Object -Property @{Name = "Minor"; Expression = {$_.Version.Split(".")[2]}} | Select-Object -ExpandProperty Minor
$script:InstallExchangeTaskName = "InstallExchange"
$script:RunOnceTaskName = "InstallExchangeMonitor"

$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 = @()

#Error Messages
$ERR_OK                         = 0
$ERR_PROBLEMADPREPARE            = 1001
$ERR_UNEXPECTEDOS               = 1002
$ERR_UNEXPTECTEDPHASE           = 1003
$ERR_PROBLEMADDINGFEATURE        = 1004
$ERR_NOTDOMAINJOINED            = 1005
$ERR_NOFIXEDIPADDRESS           = 1006
$ERR_CANTCREATETEMPFOLDER       = 1007
$ERR_UNKNOWNROLESSPECIFIED      = 1008
$ERR_NOACCOUNTSPECIFIED         = 1009
$ERR_RUNNINGNONADMINMODE        = 1010
$ERR_AUTOPILOTNOSTATEFILE       = 1011
$ERR_ADMIXEDMODE                = 1012
$ERR_ADFORESTLEVEL              = 1013
$ERR_INVALIDCREDENTIALS         = 1014
$ERR_CANTLOADSERVERMANAGER      = 1015
$ERR_MDBDBLOGPATH               = 1016
$ERR_MISSINGORGANIZATIONNAME    = 1017
$ERR_ORGANIZATIONNAMEMISMATCH   = 1018
$ERR_PROBLEMPACKAGEDL           = 1120
$ERR_PROBLEMPACKAGESETUP        = 1121
$ERR_PROBLEMPACKAGEEXTRACT      = 1122
$ERR_PROBLEMFILTERPACKDL        = 1131
$ERR_PROBLEMFILTERPACKSETUP     = 1132
$ERR_PROBLEMFILTERPACKSP1DL     = 1133
$ERR_PROBLEMFILTERPACKSP1SETUP  = 1134
$ERR_BADFORESTLEVEL             = 1151
$ERR_BADDOMAINLEVEL             = 1152
$ERR_NOTSUPPORTED               = 1153
$ERR_MISSINGEXCHANGESETUP       = 1201
$ERR_PROBLEMEXCHANGESETUP       = 1202
$ERR_PROBLEMSAVECONIFG          = 1203

$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
$EX2016STOREEXE_PRE             = "15.01.0225.016"
#$EX2016STOREEXE_RTM = "15.01.0225.042"
$EX2016STOREEXE_CU1             = "15.01.0396.030"
$EX2016STOREEXE_CU2             = "15.01.0466.034"
$EX2016STOREEXE_RTM             = "15.01.0225.037" #This matches the installer 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";
    $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"
}

# 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

# Exchange ISO Locations

$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: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"

#endregion

Function Write-Log {
    <#
        .SYNOPSIS
            Writes to a log file and echoes the message to the console.
 
        .DESCRIPTION
            The cmdlet writes text or a PowerShell ErrorRecord to a log file and displays the log message to the console at the specified logging level.
 
        .PARAMETER Message
            The message to write to the log file.
 
        .PARAMETER ErrorRecord
            Optionally specify a PowerShell ErrorRecord object to include with the message.
 
        .PARAMETER Level
            The level of the log message, this is either INFO, WARNING, ERROR, DEBUG, or VERBOSE. This defaults to INFO.
 
        .PARAMETER Path
            The path to the log file. This defaults to $script:LogPath which is "$env:SystemDrive\InstallExchange.log".
 
        .PARAMETER NoInfo
            Specify to not add the timestamp and log level to the message being written.
 
        .INPUTS
            System.String
 
                The log message can be piped to Write-Log
 
        .OUTPUTS
            None
 
        .EXAMPLE
            try {
                $Err = 10 / 0
            }
            catch [Exception]
            {
                Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level ERROR
            }
 
            Writes an ERROR log about dividing by 0 to the default log path.
 
        .EXAMPLE
            Write-Log -Message "The script is starting"
 
            Writes an INFO log to the default log path.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 2)]
        [ValidateSet("INFO", "WARNING", "ERROR", "DEBUG", "VERBOSE")]
        [System.String]$Level = "INFO",
        [Parameter(Mandatory=$true, Position = 0, ValueFromPipeline = $true)]
        [System.String]$Message,
        [Parameter(Position = 1)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord,
        [Parameter()]
        [System.String]$Path,
        [Parameter()]
        [switch]$NoInfo
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($Path)) {
            $Path = $script:LogPath
        }
    }

    Process {
        if ($ErrorRecord -ne $null) {
            $Message += "`r`n"
            $Message += ("Exception: `n" + ($ErrorRecord.Exception | Select-Object -Property * | Format-List | Out-String) + "`n")
            $Message += ("Category: " + ($ErrorRecord.CategoryInfo.Category.ToString()) + "`n")
            $Message += ("Stack Trace: `n" + ($ErrorRecord.ScriptStackTrace | Format-List | Out-String) + "`n")
            $Message += ("Invocation Info: `n" + ($ErrorRecord.InvocationInfo | Format-List | Out-String))
        }
        
        if ($NoInfo) {
            $Content = $Message
        }
        else {
            $Content = "$(Get-Date) : [$Level] $Message"
        }

        Add-Content -Path $Path -Value $Content

        switch ($Level) {
            "INFO" {
                Write-Host $Content
                break
            }
            "WARNING" {
                Write-Warning -Message $Content
                break
            }
            "ERROR" {
                Write-Error -Message $Content
                break
            }
            "DEBUG" {
                Write-Debug -Message $Content
                break
            }
            "VERBOSE" {
                Write-Verbose -Message $Content
                break
            }
            default {
                Write-Warning -Message "Could not determine log level to write."
                Write-Host $Content
                break
            }
        }
    }

    End {

    }
}

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.
 
        .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", "2016_RTM","2016_CU1","2016_CU2")]
        [System.String]$Version,
        [Parameter(ParameterSetName="AWS",Mandatory=$true)]
        [System.String]$BucketName,
        [Parameter(ParameterSetName="AWS",Mandatory=$true)]
        [System.String]$Key
    )

    Begin {}

    Process {
        if (!(Test-Path -Path $Destination)) {
            Write-Log -Message "Creating download destination at $Destination."
            New-Item -Path $Destination -ItemType Directory -Force -Confirm:$false | Out-Null
        }

        if ($PSCmdlet.ParameterSetName -eq "Version") {
            switch ($Version) {
                "2013_CU12" {
                    $Source = $script:EX2013CU12_EXE
                    break
                }
                "2013_CU13" {
                    $Source = $script:EX2013CU13_EXE
                    break
                }
                "2016_RTM" {
                    $Source = $script:EX2016RTM_EXE
                    break
                }
                "2016_CU1" {
                    $Source = $script:EX2016CU1_ISO
                    break
                }
                "2016_CU2" {
                    $Source = $script:EX2016CU2_ISO
                    break
                }
                default {
                    $Source = ""
                    break
                }
            }
        }

        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 $_
                }
            }
        }
        else {
            $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()
            }
        }

        $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) {
                Write-Log -Message "Job to copy ISO contents failed with error: $($Job.ChildJobs[0].Error)" -Level ERROR
                Exit 1
            }

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

        $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"
            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
        }
    }

    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 "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[$FileVersion]) {
            $Result = "$FileVersion ($($Versions[$FileVersion]))"
        }
        Else {
            $Result = "$FileVersion (Unknown Version)"
        }
    }

    End {
        Write-Output $Result
    }
}

Function Get-PSExecutionPolicy {
    <#
        .SYNOPSIS
            Gets the current PowerShell script execution policy for the computer.
 
        .DESCRIPTION
            Retrieves the execution policy from HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-PSExecutionPolicy
 
            This might return "Unrestricted" or "Bypass".
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process 
    {       
        $PSPolicy= Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell" -Name ExecutionPolicy -ErrorAction SilentlyContinue

        if (![System.String]::IsNullOrEmpty($PSPolicy)) {
            Write-Log -Message "PowerShell Execution Policy is set to $($PSPolicy.ExecutionPolicy) through GPO" -Level "WARNING"
        }
        Else {
            Write-Log -Message "PowerShell Execution Policy not configured through GPO" -Level "VERBOSE"
        }
    }

    End {
        Write-Output $PSPolicy
    }
}

Function Test-LocalAdmin {
    <#
        .SYNOPSIS
            Tests for local admin credentials of the current user.
 
        .DESCRIPTION
            Tests for membership in the local administrators group.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-LocalAdmin
 
            This returns true if the user is a local administrator or false otherwise.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
    }

    Process {
        $CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
        Write-Output ($CurrentPrincipal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))
    }

    End {

    }
}

Function Test-PendingReboots {
    <#
        .SYNOPSIS
            Determines if there are any pending reboot operations.
 
        .DESCRIPTION
            This cmdlet checks pending reboots from Windows Update, File Rename Operations, Computer Renaming, SCCM, and Component Based Servicing.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-PendingReboots
 
            Returns true if there are pending reboots or false otherwise.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
        $CbsReboot = $false
        $SccmReboot = $false
    }

    Process {
        $OSBuild = Get-WmiObject -Class Win32_OperatingSystem -Property BuildNumber -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BuildNumber
        
        $WindowsUpdateReboot = Test-Path -Path "HKLM:\\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"

        #if OS is Vista/2008 or greater
        if ([Int32]$OSBuild -ge 6001)
        {
            $CbsReboot = (Get-ChildItem -Path "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing" | Select-Object -ExpandProperty Name | Where-Object {$_ -contains "RebootPending"}) -ne $null
        }

        $FileRename = Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager" -Name "PendingFileRenameOperations" -ErrorAction SilentlyContinue

        $FileNameReboot = ($FileName -ne $null)

        $ComputerRenameReboot = (Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ActiveComputerName" -Name ComputerName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ComputerName) -ne 
            (Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName" -Name ComputerName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ComputerName)

        try
        {
            $SccmClientSDK = Invoke-WmiMethod -Class CCM_ClientUtilities -Name "DetermineIfRebootPending" -Namespace "ROOT\\ccm\\ClientSDK" -ErrorAction Stop
            $SccmReboot = ($SccmClientSDK.IsHardRebootPending -or $SccmClientSDK.RebootPending)
        }
        catch {}

        $Reboots = @{"Component Based Servicing" = $CbsReboot; "File Rename" = $FileNameReboot; "Computer Rename" = $ComputerRenameReboot; "Windows Update" = $WindowsUpdateReboot; "SCCM" = $SccmReboot}
    }

    End {
        $Reboots.GetEnumerator() | Where-Object {$_.Value -eq $true} | ForEach-Object {
            Write-Log -Message "Pending reboot for $($_.Name)." -Level "VERBOSE"
        }

        Write-Output ($Reboots.ContainsValue($true))
    }
}

Function Set-UAC {
    <#
        .SYNOPSIS
            Sets the User Account Control to enabled or disabled.
 
        .DESCRIPTION
            This cmdlet sets the User Account Control to enabled or disabled.
 
        .PARAMETER Enabled
            Specify whether UAC should be enabled or disabled.
 
        .INPUTS
            Systyem.Boolean
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-UAC -Enabled $false
 
            Disables UAC.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline = $true, Position = 0)]
        [System.Boolean]$Enabled
    )
    
    Begin {
        Write-Log -Message "Setting User Account Control to Enabled = $Enabled." -Level VERBOSE
    }

    Process {
        New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -Value ([Int32]$Enabled) -ErrorAction SilentlyContinue| out-null
    }

    End {}
}

Function Set-IEESC {
    <#
        .SYNOPSIS
            Sets Internet Explorer Enhanced Security Configuration to enabled or disabled.
 
        .DESCRIPTION
            This cmdlet sets Internet Explorer Enhanced Security Configuration to enabled or disabled.
 
        .PARAMETER Enabled
            Specify whether IEESC should be enabled or disabled.
 
        .INPUTS
            Systyem.Boolean
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-IEESC -Enabled $false
 
            Disables IEESC.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.Boolean]$Enabled
    )

    Begin {}

    Process {
        Write-Log "Setting IE Enhanced Security Configuration to Enabled = $Enabled."
        $AdminKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}"
        $UserKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}"
        Set-ItemProperty -Path $AdminKey -Name "IsInstalled" -Value ([Int32]$Enabled)
        Set-ItemProperty -Path $UserKey -Name "IsInstalled" -Value ([Int32]$Enabled)
        if ((Get-Process -Name Explorer -ErrorAction SilentlyContinue) -ne $null) {
            Stop-Process -Name Explorer
        }
    }

    End {}
}

Function Test-Credentials {
    <#
        .SYNOPSIS
            Validates a set of credentials.
 
        .DESCRIPTION
            This cmdlet takes a set of credentials and validates them against Active Directory.
 
        .PARAMETER EncryptedPassword
            An encrypted string representing the password. This string should be encrypted using the ConvertFrom-SecureString cmdlet under the current user's context.
 
        .PARAMETER UserName
            The name of the user account. This can be specified as either DOMAIN\UserName or just as UserName and the domain will default to the current user domain.
 
        .PARAMETER Password
            An unencrypted string.
 
        .PARAMETER Credential
            A PSCredential object of the credentials to validate.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-Credentials -UserName administrator -Password MyP@$$w0rD
 
            Validates the provided credentials using the current user domain.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding(DefaultParameterSetName="Encrypted")]
    Param(
        [Parameter(Mandatory=$true, ParameterSetName="Encrypted")]
        [System.String]$EncryptedPassword,
        [Parameter(Mandatory=$true, ParameterSetName="PlainText")]
        [System.String]$Password,
        [Parameter(Mandatory=$true, ParameterSetName="Encrypted")]
        [Parameter(Mandatory=$true, ParameterSetName="PlainText")]
        [System.String]$UserName,
        [Parameter(Mandatory=$true, ParameterSetName="Credential")]
        [PSCredential]$Credential
    )

    Begin {
        $Result = $false
    }

    Process {

        switch ($PSCmdlet.ParameterSetName) {
            "Encrypted" {
                $PlainTextPassword = Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String $EncryptedPassword)
                break
            }
            "PlainText" {

                break
            }
            "Credential" {
                $UserName = $Credential.UserName
                $PlainTextPassword = Convert-SecureStringToString -SecureString $Credential.Password
                break
            }
            
        }

        if($UserName.Contains("\")) {
            $Parts= $UserName.Split("\")
            $Domain = $Parts[0]
            $UserName= $Parts[1]
        }
        else {
            $Domain = $env:USERDOMAIN
        }

        Write-Log -Message "Testing credentials for user $UserName in domain $Domain."

        try {
            [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | Out-Null

            [System.DirectoryServices.AccountManagement.PrincipalContext]$Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $Domain)
            $Result = $Context.ValidateCredentials($UserName, $PlainTextPassword)
            
            Write-Log -Message "Provided credentials are valid : $Result"
        }
        catch [Exception] {
            Write-Log -Message "Error checking credentials."
            Write-Log -ErrorRecord $_ -Level WARNING
        }
    }

    End {
        Write-Output $Result
    }
}

Function Set-OpenFileSecurityWarning {
    <#
        .SYNOPSIS
            Enables or disables file security warnings from items downloaded from the internet.
 
        .DESCRIPTION
            This cmdlet enables or disables file security warnings from items downloaded from the internet.
 
        .PARAMETER Enable
            Specify to enable the security warnings.
 
        .PARAMETER Disable
            Specify to disable the security warnings.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-OpenFileSecurityWarning -Disable
 
            Disables the security warning when opening files from the internet.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true, ParameterSetName="Enable")]
        [switch]$Enable,
        [Parameter(Mandatory=$true, ParameterSetName="Disable")]
        [switch]$Disable    
    )

    Begin {}

    Process {
        if ($Enable) {
            Write-Log -Message "Enabling File Security Warning dialog." -Level VERBOSE
            Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue
            Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue
            Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue
            Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue
        }
        elseif ($Disable) {
            Write-Log -Message "Disabling File Security Warning dialog." -Level VERBOSE
            New-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -ErrorAction SilentlyContinue | Out-Null
            New-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -Value ".exe;.msp;.msu;.msi" -ErrorAction SilentlyContinue | Out-Null
            New-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -ErrorAction SilentlyContinue | Out-Null
            New-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -Value 1 -ErrorAction SilentlyContinue | Out-Null
            Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue
            Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue
        }
    }

    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 $NamingContext
        }
        catch [Exception] {
            Write-Log -Message "Could not retrieve the forest root naming context." -ErrorRecord $_ -Level ERROR
        }
    }
    
    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 $NamingContext
        }
        catch [Exception] {
            Write-Log -Message "Could not retrieve the domain root naming context." -ErrorRecord $_ -Level ERROR
        }
    }

    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 $Forest.ForestModeLevel
        }
        catch [Exception] {
            Write-Log "Could not retrieve forest functional level." -ErrorRecord $_ -Level ERROR
        }
    }

    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 ($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 {
        Write-Log -Message "Getting Exchange Organization name from the msExchOrganizationContainer object class."
    }

    Process {     
        try {
            $NC = Get-ForestRootNC
            $Path = "LDAP://CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC"
            if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) {
                $ExOrgContainer = [ADSI]$Path
                try {
                    $Result = $ExOrgContainer.PSBase.Children | Where-Object { $_.objectClass -eq 'msExchOrganizationContainer' } | Select-Object -ExpandProperty Name
                }
                catch [Exception] {
                    $Result = $null
                }
            }
            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
        }
    }
    
    End {
        Write-Output $Result
    }
}

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 {
        $NC= Get-ForestRootNC
        $Path = "LDAP://CN=$Organization,CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC"
        $Result = [System.DirectoryServices.DirectoryEntry]::Exists($Path)
    }

    End {
        Write-Output $Result
    }
}

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 {
        
        try {
            $NC= Get-ForestRootNC
            $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 {
                $Result = $null
            }
        }
        catch [Exception] {
            Write-Log -Message "Could not retrieve Exchange Forest Level." -ErrorRecord $_ -Level VERBOSE
            $Result = $null
        }
    }

    End {
        Write-Output $Result
    }
}

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 {
        try {
            $NC = Get-DomainNC
            $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
        }
    }

    End {
        Write-Output $Result
    }
}

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 {
        $NC= Get-ForestRootNC
        $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()
        }
    }

    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
        $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 $_
            }
        }
    }

    End {}
}

Function Get-LocalFQDNHostname {
    <#
        .SYNOPSIS
            Gets the FQDN of the local host.
 
        .DESCRIPTION
            This cmdlet get the FQDN of the local host from DNS.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-LocalFQDNHostname
 
            Returns the local computer's FQDN.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        Write-Output ([System.Net.Dns]::GetHostByName($env:COMPUTERNAME)).HostName
    }

    End {}
}

Function Set-RunOnceScript {
    <#
        .SYNOPSIS
            Adds a RunOnce script that launches a PowerShell script at user logon.
 
        .DESCRIPTION
            This cmdlet adds a RunOnce script that launches on user logon. If the specified Name already exists as a RunOnce entry, it is removed first.
 
        .PARAMETER Command
            The command to run. This can be the path to a script file, or native PowerShell commands.
 
        .PARAMETER StoreAsPlainText
            Stores the commands as plain text instead of Base64.
 
        .PARAMTER RunFile
            Specifies that the command parameter was the path to a script file and not native PowerShell commands.
 
        .PARAMETER Name
            The name of the RunOnce entry in the registry. This defaults to $script:RunOnceTaskName which is InstallExchangeMonitor.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-RunOnceScript -Command "c:\test.ps1" -RunFile
 
            Configures the RunOnce setting to run the c:\test.ps1 file when a user logs on.
 
        .EXAMPLE
            Set-RunOnceScript -Command "Get-Service" -Name "ListServices"
 
            Configures the RunOnce setting to run the Get-Service cmdlet when a user logs on.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/26/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.String]$Command,
        [Parameter(ParameterSetName="Text")]
        [switch]$StoreAsPlainText,
        [Parameter(ParameterSetName="File")]
        [switch]$RunFile,
        [Parameter()]
        [System.String]$Name = $script:RunOnceTaskName
    )

    Begin {

    }

    Process {
        $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
        
        if ([System.String]::IsNullOrEmpty($Name))
        {
            $Name = $script:RunOnceTaskName
        }

        Remove-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue

        $RunOnce = "$PSHome\PowerShell.exe -NoProfile -NoLogo -NoExit -ExecutionPolicy Unrestricted"

        if ($StoreAsPlainText) {        
            $Command = $Command.Replace("`"", "\`"").Replace("`n","").Replace("`r","").Replace("`t","")
            #$RunOnce += " -Command `"& {$Command}`""
            $RunOnce += " -Command `"$Command`""
        }
        elseif ($RunFile) {
            $RunOnce += " -File `"$Command`""
        }
        else {
            $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
            $EncodedCommand = [System.Convert]::ToBase64String($Bytes)
            $RunOnce += " -EncodedCommand $EncodedCommand"
        }

        if (!(Test-Path -Path $Path)) {
            New-Item -Path $Path | Out-Null
        }
        
        Write-Log -Message "Setting RunOnce: $RunOnce" -Level VERBOSE
        New-ItemProperty -Path $Path -Name $Name -Value "$RunOnce" -PropertyType String | Out-Null
        Write-Log -Message "Successfully set RunOnce." -Level VERBOSE
    }
    End {

    }
}

Function Set-Pagefile {
    <#
        .SYNOPSIS
            Configures the size of the page file for optimal use by Exchange.
 
        .DESCRIPTION
            This cmdlet sets the page file to the size of the computer system's RAM plus 10MB up to a size of 32GB + 10MB as both the initial and maximum size.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-Pagefile
 
            Sets the page file size manually.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param()

    Begin {}

    Process {
        Write-Log -Message "Checking Pagefile Configuration" -Level VERBOSE
        $CS = Get-WmiObject -Class Win32_ComputerSystem -EnableAllPrivileges

        if ($CS.AutomaticManagedPagefile) {
            Write-Log -Message "System configured to use Automatic Managed Pagefile, reconfiguring"

            try {
                $CS.AutomaticManagedPagefile = $false
                # RAM + 10 MB, with maximum of 32GB + 10MB
                $InstalledMem = $CS.TotalPhysicalMemory
                $DesiredSize = (($InstalledMem + 10MB), (32GB+10MB) | Measure-Object -Minimum).Minimum / 1MB
                $tmp = $CS.Put()
                $CPF = Get-WmiObject -Class Win32_PageFileSetting
                $CPF.InitialSize= $DesiredSize
                $CPF.MaximumSize= $DesiredSize
                $tmp = $CPF.Put()
            }
            catch [Exception] {
                Write-Log -Message "Problem reconfiguring pagefile." -ErrorRecord $_ -Level WARNING
            }

            $CPF= Get-WmiObject -Class Win32_PageFileSetting
            Write-Log -Message "Pagefile set to manual, initial/maximum size: $($CPF.InitialSize)MB / $($CPF.MaximumSize)MB." 
        }
        else {
            Write-Log -Message "Manually configured page file, skipping configuration" -Level VERBOSE
        }
    }
    
    End {
    }
}

Function Set-HighPerformancePowerPlan {
    <#
        .SYNOPSIS
            Enables the high performance power plan on the computer.
 
        .DESCRIPTION
            This cmdlet sets the active power plan to the High Performance setting.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-HighPerformancePowerPlan
 
            Sets the High Performance plan to active.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        Write-Log -Message "Configuring Power Plan" -Level VERBOSE
        $PowerPlan = Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan -Filter "ElementName = 'High Performance'"          
        $Temp = Invoke-CimMethod -InputObject $PowerPlan -MethodName Activate        
        $CurrentPlan = Get-WmiObject -Namespace root\cimv2\power -Class Win32_PowerPlan | Where-Object { $_.IsActive }
        Write-Log -Message "Power Plan active: $($CurrentPlan.ElementName)"
    }

    End {}
}

Function Get-NETVersion {
    <#
        .SYNOPSIS
            Gets the current version of .NET version 4 installed.
 
        .DESCRIPTION
            This cmdlet gets the current version of .NET version 4 installed from the registry at HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Int
 
        .EXAMPLE
            Get-NETVersion
 
            Retrieves the .NET version 4 specific version.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        $NetVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release
        Write-Log -Message ".NET version installed is $NetVersion." -Level VERBOSE
    }

    End {
        Write-Output ([int]$NetVersion)
    }
}

Function Set-NET461InstallBlock {
    <#
        .SYNOPSIS
            Sets a temporary installation block for .NET version 4.6.1 (KB3133990).
 
        .DESCRIPTION
            This cmdlet sets a temporary installation block for .NET 4.6.1 which is sometimes needed is a program is not compatible with this version and you don't want it to be accidentally installed
            through automatic updates.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-NET461InstallBlock
 
            Blocks the installation of .NET 4.6.1
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {}

    Process {
        Write-Log -Message "Set temporary installation block for .NET Framework 4.6.1 (KB3133990)."
        $RegKey = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\WU"
        $RegName= "BlockNetFramework461"

        if (!(Test-Path -Path $RegKey)) {
            New-Item -Path "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP" -Name "WU" | Out-Null
        }

        if ((Get-ItemProperty -Path $RegKey -Name $RegName -ErrorAction SilentlyContinue) -eq $null) {
            New-ItemProperty -Path $RegKey -Name $RegName -Value 1 -PropertyType DWORD  | Out-Null
        }

        if ((Get-ItemProperty -Path $RegKey -Name $RegName -ErrorAction SilentlyContinue) -eq $null) {
            Write-Log -Message "Unable to set registry key $RegKey\$RegName." -Level WARNING 
        }
    }

    End {}
}

Function Start-ProcessWait {
    <#
        .SYNOPSIS
            Starts a new process and waits for it to complete.
 
        .DESCRIPTION
            This cmdlet starts a new process using .NET System.Diagnostics.Process and waits for it to complete. It optionally writes the standard out of the process to the log file.
 
        .PARAMETER FilePath
            The path to the executable, script, msi, msu, etc to be executed.
 
        .PARAMETER ArgumentList
            An array of arguments to run with the file being executed. This defaults to an empty array.
 
        .PARAMTER EnableLogging
            Specify to write standard output or standard errors to the log file.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-ProcessWait -FilePath "c:\installer.msi" -EnableLogging -ArgumentList @("/qn")
 
            Launches a quiet installation from installer.msi with a no restart option. Logging is also enabled.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.String]$FilePath,
        [Parameter()]
        [System.String[]]$ArgumentList = @(),
        [Parameter()]
        [switch]$EnableLogging
    )

    Begin {
        if ($ArgumentList -eq $null) {
            $ArgumentList = @()
        }
    }

    Process {
        Write-Log -Message "Executing $FilePath $($ArgumentList -Join " ")" -Level VERBOSE
        
        if (Test-Path -Path "$FilePath") {
            $FileInfo = New-Object -TypeName System.IO.FileInfo($FilePath)

            [System.Diagnostics.Process]$Process = New-Object -TypeName System.Diagnostics.Process
            $Process.StartInfo.RedirectStandardOutput = $true
            $Process.StartInfo.UseShellExecute = $false
            $Process.StartInfo.CreateNoWindow = $true
            $Process.StartInfo.RedirectStandardError = $true

            switch($FileInfo.Extension.ToUpper()) {
                ".MSU" {
                    $ArgumentList += "$FilePath"
                    $Process.StartInfo.Filename = "$env:SystemRoot\System32\WUSA.EXE"
                    $Process.StartInfo.Arguments = $ArgumentList
                    break
                }
                ".MSP" {
                    $ArgumentList += "$FilePath"
                    $ArgumentList += "/update"
                    $Process.StartInfo.Filename = "MSIEXEC.EXE"
                    $Process.StartInfo.Arguments = $ArgumentList
                    break
                }
                ".MSI" {
                    $ArgumentList += "$FilePath"
                    $Process.StartInfo.Filename = "MSIEXEC.EXE"
                    $Process.StartInfo.Arguments = $ArgumentList
                    break
                }
                default {
                    $Process.StartInfo.Filename = "$FilePath"
                    $Process.StartInfo.Arguments = $ArgumentList
                    break
                }
            }

            $Process.Start() | Out-Null
            
            if ($EnableLogging) {
                while (!$Process.HasExited) {
                     while (![System.String]::IsNullOrEmpty(($Line = $Process.StandardOutput.ReadLine()))) {
                        Write-Log -Message $Line -NoInfo
                    }

                    Start-Sleep -Milliseconds 100
                }

                if ($Process.ExitCode -ne 0) {
                    $Line = $Process.StandardError.ReadToEnd()
                    if (![System.String]::IsNullOrEmpty($Line)) {
                        Write-Log -Message $Line -Level ERROR -NoInfo
                    }
                }
                else {
                    $Line = $Process.StandardOutput.ReadToEnd()
                    if (![System.String]::IsNullOrEmpty($Line)) {
                        Write-Log -Message $Line -NoInfo
                    }
                }
            }
            else {
                $Process.WaitForExit()
            }
        }
        else {
            Write-Log -Message "$FilePath not found." -Level WARNING
        }
    }

    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
        $Rule = New-Object System.Security.AccessControl.RegistryAccessRule ("NETWORK SERVICE","ReadKey","Allow")
        $Acl.SetAccessRule($Rule)
        $Acl | Set-Acl -Path $KeyParent
    }

    End {}
}

Function Test-PackageInstallation {
    <#
        .SYNOPSIS
            Tests for the installation of the specified software or update.
 
        .DESCRIPTION
            This cmdlet evaluates Win32_QuickFixEngineering, HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall, and
            HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products for a matching product.
 
        .PARAMETER PackageId
            The Id of the installed package, software, or update.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-PackageInstallation -PackageId KB2803757
 
            Tests for the installation of KB2803757.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

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

    Begin {}

    Process {
        $PresenceKey = $null
        $PresenceKey = Get-WmiObject -Class Win32_quickfixengineering -ErrorAction SilentlyContinue | Where-Object { $_.HotfixID -eq $PackageId } | Select-Object -ExpandProperty HotfixID
        
        if ([System.String]::IsNullOrEmpty($PresenceKey)) {
            $Result = Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$PackageId"
            
            if ($Result -eq $false) {
                # Alternative (seen KB2803754, 2802063 register here)
                $Result = Test-Path -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$PackageId"
                
                if ($Result -eq $false) {
                    # Alternative (Office2010FilterPack SP1)
                    $Result = Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\$PackageId"
                }
            }
        }
        else {
            $Result = $true
        }
    }

    End {
        Write-Output $Result
    }
}

Function Get-Package {
    <#
        .SYNOPSIS
            Retrieves a specified package from the internet.
 
        .DESCRIPTION
            This cmdlet tests for the presence of the desired package name, and if it is not present, downloads it from the given Url.
 
            Hotfixes that download with a _zip in the filename, but have a .exe extension, will be automatically expanded to a true zip file.
 
        .PARAMETER PackageName
            The name of the package.
 
        .PARAMETER Destination
            Where the package should be downloaded to.
 
        .PARAMETER Url
            The source to download the package from.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Get-Package -PackageName "Test App" -Url "http://contoso.com/testapp.zip" -Destination "c:\testapp.zip"
 
            Enables the OneNote and Publisher IFilters.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter()]
        [System.String]$PackageName,
        [Parameter()]
        [System.String]$Url,
        [Parameter(Mandatory=$true)]
        [System.String]$Destination
    )

    Begin {
        $Result = @()

        if (![System.String]::IsNullOrEmpty($PackageName)) {
            Write-Log -Message "Processing package $PackageName."
        }
    }

    Process {
        if (!(Test-Path -Path $Destination)) {
            if (![System.String]::IsNullOrEmpty($Url)) {
                Write-Log "$Destination not present, downloading from $Url."
                try {
                    $WebClient = New-Object -TypeName System.Net.WebClient
                    $WebClient.DownloadFile($Url, $Destination)

                    $FileInfo = New-Object -TypeName System.IO.FileInfo($Destination)

                    if ($FileInfo.Name.Contains("_zip")) {
                        try {
                            Write-Log -Message "Expanding Hotfix $($FileInfo.Name)."
                            if (!$Destination.EndsWith(".zip")) {
                                $Destination = Rename-Item -Path $Destination -NewName "$Destination.zip" -PassThru | Select-Object -ExpandProperty FullName
                            }

                            [System.IO.Compression.ZipArchive]$Zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
                            $Contents = $Zip.Entries | Select-Object -Property @{Name = "Path"; Expression = {"$($FileInfo.DirectoryName)\$($_.FullName)"}} | Select-Object -ExpandProperty Path
                            $Zip.Dispose()
                            [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $FileInfo.DirectoryName)

                            Write-Log -Message "Successfully expanded files $($Contents -join `",`")"

                            $Result = $Contents
                        }
                        catch [Exception] {
                            Write-Log -Message "Error expanding zip file $Destination." -Level WARNING -ErrorRecord $_
                        }
                    }
                    else {
                        $Result = @($Destination)
                    }
                }
                catch [Exception] {
                    Write-Log -Message "Problem downloading file from $Url using BITS." -Level WARNING -ErrorRecord $_
                }
            }
            else {
                Write-Log -Message "$Destination not present and no URL provided, not downloading." -Level WARNING
                $Result = @()
            }
        }
        else {
            Write-Log -Message "$Destination is present, no need to download." -Level VERBOSE
            $Result = @($Destination)
        }
    }

    End {
        Write-Output $Result
    }
}

Function Get-FileVersion {
    <#
        .SYNOPSIS
            Gets the version of a specific file or file running a Windows service from its metadata.
 
        .DESCRIPTION
            This cmdlet gets the FileVersion data from a specified file or file running a service.
 
        .PARAMETER Path
            The path to the file.
 
        .PARAMETER Service
            The name of the service.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Get-FileVersion -Path "c:\installer.exe"
 
            Gets the file version of installer.exe.
 
        .EXAMPLE
            Get-FileVersion -Service lmhosts
 
            Gets the file version of the svchost.exe running the lmhosts service.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ParameterSetName="File",ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]$Path
    )

    DynamicParam
    {
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $Services = Get-Service | Select-Object -ExpandProperty Name

        $ValidateSet = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($Services)

        [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
        $Attributes.ParameterSetName = "Service"
        $Attributes.Mandatory = $true
        $Attributes.ValueFromPipeline = $true
        $Attributes.Position = 0
            
        $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollection.Add($Attributes)
        $AttributeCollection.Add($ValidateSet)

        [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("ServiceName", [System.String], $AttributeCollection)
        $ParamDictionary.Add("ServiceName", $DynParam)

        return $ParamDictionary
    }

    Begin {}

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            "File" {
                break
            }
            "Service" {
                $Path = (Get-WmiObject -Class Win32_Service -Filter "Name = `"$($PSBoundParameters.ServiceName)`"" | Select-Object -ExpandProperty PathName).Trim("`"")
                break
            }
            default {
                throw "Could not determine parameter set name from given parameters."
            }
        }

        $Version = New-Object -TypeName System.IO.FileInfo($Path) | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
    }

    End {
        Write-Output $Version
    }
}

Function Start-PackageInstallation {
    <#
        .SYNOPSIS
            Installs the specified package.
 
        .DESCRIPTION
            This cmdlet launches the installation of a specified package.
 
        .PARAMETER PackageId
            The PackageId of the package to test if the package is already installed.
 
        .PARAMETER PackageName
            The name of the package to install, can be any text you want to identify the package in the logs.
 
        .PARAMETER Destination
            The location to download the installation files to.
 
        .PARAMETER Url
            The source path to download the installation files from.
 
        .PARAMETER Arguments
            The arguments to be used with the installation file.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Start-PackageInstallation -PackageId "KB123456" -PackageName "Another update" -Destination "c:\kb123456.msu" -Url "http://contoso.com/kb123456.msu" -ArgumentList @("\qn")
 
            Installs the specified KB.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.String]$PackageId,
        [Parameter(Mandatory=$true)]
        [System.String]$PackageName,
        [Parameter(Mandatory=$true)]
        [System.String]$Destination,
        [Parameter(Mandatory=$true)]
        [System.String]$Url,
        [Parameter()]
        [System.String[]]$Arguments = @()
    )

    Begin {}

    Process {
        Write-Log -Message "Processing $Package ($PackageId)"

        if (!(Test-PackageInstallation -PackageId $PackageId)) {

            Write-Log -Message "Package not detected, installing."

            $Contents = @()

            if (!(Test-Path -Path $Destination)) {
                # Download & Extract
                $Contents = Get-Package -Package $PackageName -Url $Url -Destination $Destination

                if ($Contents.Count -eq 0) {
                    Write-Log -Message "Problem downloading/accessing $PackageName" -Level ERROR
                    throw "Problem downloading/accessing $PackageName"
                }
            }
            else {
                $Contents += $Destination
            }
               
            Write-Log -Message "Installing $PackageName"

            foreach ($Item in $Contents) {
                $Lower = $Item.ToLower()
                if ($Lower.EndsWith(".exe") -or $Lower.EndsWith(".msi") -or $Lower.EndsWith(".msu") -or $Lower.EndsWith(".msp")) {
                    Start-ProcessWait -FilePath $Item -ArgumentList $Arguments -EnableLogging
                }
            }

            if (!(Test-PackageInstallation -PackageId $PackageId)) {
                Write-Log -Message "Problem installing $PackageName after the install steps were run, did not find the package Id $PackageId." -Level ERROR
                throw "Problem installing $PackageName after the install steps were run, did not find the package Id $PackageId."
            }
            else {
                Write-Log -Message "$PackageName successfully installed."
            }
        }
        else {
            Write-Log -Message "$PackageName already installed" -Level VERBOSE
        }  
    }
    
    End {}  
}

Function Disable-SSLv3 {
    <#
        .SYNOPSIS
            Completely disables the use of SSLv3.
 
        .DESCRIPTION
            This cmdlet disables SSLv3 by use of both the client and server components.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Disable-SSLv3
 
            Disables SSLv3 on the system.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    Param(
    )

    Begin {
        #Disable SSLv3 to protect against POODLE scan
        $ServerRegKey = "HKLM:\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server"
        $ClientRegKey = "HKLM:\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client"
        $ServerRegName = "Enabled"
        $ClientRegName = "DisabledByDefault"
    }

    Process {
        Write-Log -Message "Disabling SSLv3 protocol."

        if (!(Test-Path -Path $ServerRegKey)) {
            New-Item -Path $ServerRegKey | Out-Null
        }

        New-ItemProperty -Path $ServerRegKey -Name $ServerRegName -Value 0 -PropertyType DWORD | Out-Null

        if (!(Test-Path -Path $ClientRegKey)) {
            New-Item -Path $ClientRegKey | Out-Null
        }

        New-ItemProperty -Path $ClientRegKey -Name $ServerRegName -Value 0 -PropertyType DWORD | Out-Null
        New-ItemProperty -Path $ClientRegKey -Name $ClientRegName -Value 1 -PropertyType DWORD | Out-Null
    }

    End {
        Write-Log -Message "Successfully disabled SSLv3."
    }        
}
       
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 {
        if ([System.String]::IsNullOrEmpty($TaskName))
        {
            $TaskName = $script:InstallExchangeTaskName
        }

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

    Process {
        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
        }
    }

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

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
            None
 
        .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()]
        [PSCredential]$Credential
    )

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

    Process {

        Write-Log -Message "Performing sanity checks."

        Write-Log -Message "Checking temporary installation folder."
        if (!(Test-Path -Path "$($Config.TempDirectory)")) {
            try {
                New-Item -Path "$($Config.TempDirectory)" -ItemType Directory | Out-Null
            }
            catch [Exception] {
                Write-Log -Message "Error creating Temporary Installation Directory at $($Config.TempDirectory)." -Level ERROR
                Exit $ERR_CANTCREATETEMPFOLDER
            }
        }

        Write-Log -Message "Checking Operating System $($MajorOSVersion).$($MinorOSVersion)" 
        if (($MajorOSVersion -ne $WS2012R2_MAJOR) -and ($MajorOSVersion -ne $WS2012_MAJOR) -and ($MajorOSVersion -eq $WS2008R2_MAJOR -and $MinorOSVersion -lt 7601)) {
            Write-Log -Message "Windows Server 2008 R2 SP1, Windows Server 2012 or Windows Server 2012 R2 is required, but not detected" -Level ERROR
            Exit $ERR_UNEXPECTEDOS
        }

        Write-Log -Message "Checking privilege elevation..."
        if (!(Test-LocalAdmin)) {
            Write-Log -Message "Script requires running with elevated privileges." -Level ERROR
            Exit $ERR_RUNNINGNONADMINMODE
        }
        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")) {
            Write-Log -Message "Can't find Exchange setup at $($Config.SourceDirectory)\setup.exe."
            Exit $ERR_MISSINGEXCHANGESETUP
        }
        else {

            Write-Log -Message "Exchange Setup Version: $(Get-TextVersion $Config.SetupVersion)."
            Write-Log -Message "Checking roles to install."

            if ($MajorSetupVersion -ge 15.01) {
                if (!$Config.InstallMailbox) {
                    Write-Log -Message "No roles specified to install" -Level ERROR
                    Exit $ERR_UNKNOWNROLESSPECIFIED
                }
                
                if ($Config.InstallCAS) {
                    Write-Log -Message "Exchange 2016 setup detected, will ignore deprecated InstallCAS parameter." -Level WARNING
                }
            }
            else {
                if (!$Config.InstallMailbox -and !$Config.InstallCAS) {
                    Write-Log -Message "No roles specified to install" -Level ERROR
                    Exit $ERR_UNKNOWNROLESSPECIFIED
                }
            }
        }
        
        Write-Log -Message "Checking domain membership status..."
        if((Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain -eq $false) {
            Write-Log -Message "System is not domain-joined" -Level ERROR
            Exit $ERR_NOTDOMAINJOINED
        }

        Write-Log -Message "Checking NIC configuration..."
        if ((Get-WmiObject 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)) {
                Write-Log -Message "Target directory drive unavailable: ($Drive)" -Level ERROR
                Exit $ERR_MDBDBLOGPATH
            }
        }

        if ($Config.InstallMDBLogPath) {
            $Drive = Split-Path $Config.InstallMDBLogPath -Qualifier
            Write-Log -Message "Checking MDB log path..."
            if(!(Test-Path -Path $Drive)) {
                Write-Log -Message "MDB log drive unavailable: ($Drive)" -Level ERROR
                Exit $ERR_MDBDBLOGPATH
            }
        }

        if ($Config.InstallMDBDBPath) {
            $Drive = Split-Path $Config.InstallMDBDBPath -Qualifier
            Write-Log -Message "Checking MDB database path..."
            if(!(Test-Path -Path $Drive)) {
                Write-Log -Message "MDB database drive unavailable: ($Drive)" -Level ERROR
                Exit $ERR_MDBDBLOGPATH
            }
        }

        $ExOrg = Get-ExchangeOrganization
        if (![System.String]::IsNullOrEmpty($ExOrg)) {
            if(![System.String]::IsNullOrEmpty($Config.Organization)) {
                if($ExOrg -ne $Config.Organization) {
                    Write-Log -Message "OrganizationName ($($Config.Organization)) mismatches with discovered Exchange Organization name ($ExOrg)." -Level ERROR
                    Exit $ERR_ORGANIZATIONNAMEMISMATCH
                }
            }
            Write-Log -Message "Exchange Organization is: $ExOrg"
        }
        else {
            if(![System.String]::IsNullOrEmpty($Config.Organization)) {
                Write-Log -Message "Exchange Organization will be: $($Config.Organization)."
            }
            else {
                Write-Log -Message "Organization not specified and no Exchange Organization discovered." -Level ERROR
                Exit $ERR_MISSINGORGANIZATIONNAME
            }
        }

        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
                Write-Log -Message "Minimum required Forest Functional Level version is $MinForestLevel, aborting." -Level ERROR
                Exit $ERR_BADFORESTLEVEL
            }
        }
        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
                Write-Log -Message "Minimum required Domain Functional Level version is $MinDomainLevel, aborting." -Level ERROR
                Exit $ERR_BADDOMAINLEVEL
            }
        }

        Write-Log -Message "Checking domain mode"
        if ((Test-DomainNativeMode) -eq $false) {
            Write-Log -Message "Domain is in mixed mode, native mode is required" -Level ERROR
            Exit $ERR_ADMIXEDMODE
        }
        else {
            Write-Log -Message "Domain is in native mode"
        }

        Write-Log -Message "Checking Forest Functional Level"
        if ((Get-ForestFunctionalLevel) -lt $FOREST_LEVEL2003) {
            Write-Log -Message "Forest is not Functional Level 2003 or later" -Level ERROR
            Exit $ERR_ADFORESTLEVEL
        }
        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 $null -and $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) {
                Write-Log -Message "Provided credentials don't seem to be valid." -Level ERROR
                Exit $ERR_INVALIDCREDENTIALS
            } 
        }
    }

    End {
        Write-Output $true
    }
}

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) {
                Write-Log -Message "Problem updating schema, domain, or Exchange organization." -Level ERROR
                Exit $ERR_PROBLEMADPREPARE
            }
            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) {
            Write-Log -Message "Error encountered installing Exchange" -Level ERROR
            Exit $ERR_PROBLEMEXCHANGESETUP
        }
        else {
            $LocalAdmins = Get-LocalGroupMembers -LocalGroup "Administrators"
            Write-Log -Message "Current local admins: `n$($LocalAdmins -join "`n")" -Level VERBOSE
        }
    }

    End {
    }
}

Function Get-LocalGroupMembers {
    <#
        .SYNOPSIS
            Gets the members of a local group
 
        .DESCRIPTION
            This cmdlet gets the members of a local group on the local or a remote system. The values are returned as DirectoryEntry values in the format WinNT://Domain/Name.
 
        .PARAMETER LocalGroup
            The local group on the computer to enumerate.
 
        .PARAMETER ComputerName
            The name of the computer to query. This defaults to the local computer.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String[]
 
        .EXAMPLE
            Get-LocalGroupMembers -LocalGroup Administrators
 
            Gets the membership of the local administrators group on the local machine.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/25/2016
    #>
  
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [System.String]$LocalGroup,        
        [Parameter(Position=1)]
        [System.String]$ComputerName = $env:COMPUTERNAME
    )

    Begin {
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ComputerName))
        {
            $ComputerName = $env:COMPUTERNAME
        }
        $Group = [ADSI]"WinNT://$ComputerName/$LocalGroup,group"    
                                    
        $Members = $Group.Invoke("Members", $null) | Select-Object @{Name = "Name"; Expression = {$_[0].GetType().InvokeMember("ADSPath", "GetProperty", $null, $_, $null)}} | Select-Object -ExpandProperty Name                
    }

    End {
        Write-Output $Members
    }
}

Function Add-DomainMemberToLocalGroup {
    <#
        .SYNOPSIS
            Adds a domain user or group to a local group.
 
        .DESCRIPTION
            This cmdlet adds a domain user or group to a local group on a specified computer. The cmdlet returns true if the member is added or is already a member of the group.
 
            The cmdlet uses the current computer domain to identify the domain member.
 
        .PARAMETER LocalGroup
            The local group on the computer that will have a member added.
 
        .PARAMETER Member
            The domain user or group to add.
 
        .PARAMETER MemberType
            The type of the domain member, User or Group. This defaults to User.
 
        .PARAMETER ComputerName
            The name of the computer on which to add the local group member. This defaults to the local computer.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Add-DomainMemberToLocalGroup -LocalGroup Administrators -Member "Exchange Trusted Subsystem" -MemberType Group
 
            Adds the domain group to the local administrators group on the local machine.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/25/2016
    #>
  
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [System.String]$LocalGroup,
        [Parameter(Mandatory=$true,Position=1)]
        [System.String]$Member,
        [Parameter(Position=2)]
        [ValidateSet("Group", "User")]
        [System.String]$MemberType = "User",
        [Parameter(Position=3)]
        [System.String]$ComputerName = $env:COMPUTERNAME
    )

    Begin {
        Write-Log -Message "Adding $Member to $LocalGroup."
        $Success = $false
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ComputerName))
        {
            $ComputerName = $env:COMPUTERNAME
        }

        $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() | Select-Object -ExpandProperty Name
        $Group = [ADSI]"WinNT://$ComputerName/$LocalGroup,group"    
                                    
        $Members = $Group.Invoke("Members", $null) | Select-Object @{Name = "Name"; Expression = {$_[0].GetType().InvokeMember("ADSPath", "GetProperty", $null, $_, $null)}} | Select-Object -ExpandProperty Name        
        $NewMember = [ADSI]"WinNT://$Domain/$Member,$MemberType"
                                
        if ($Members -inotcontains $NewMember.Path)
        {
            try {
                $Group.Add($NewMember.Path)
                Write-Log -Message "Successfully added membership."
                $Success = $true
            }
            catch [Exception] {
                Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level ERROR
            }
        }
        else
        {
            Write-Log -Message "$($NewMember.Name) already a member of $($Group.Name)."
            $Success = $true
        }
    }

    End {
        Write-Output $Success
    }
}

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.
 
        .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()]
        [PSCredential]$Credential = [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 {
        $ScriptFullName = $MyInvocation.MyCommand.Path
        $ParameterString = $PSBoundParameters.GetEnumerator() -join " "

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

        if ($Credential -eq $null) {
            $Credential = [PSCredential]::Empty
        }

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

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

    Process {
        
        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] {
                    Write-Log -Message "Unattended specified, but no or improper credentials provided." -Level ERROR
                    Exit $ERR_NOACCOUNTSPECIFIED
                }
            }

            $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 = New-Object -TypeName System.IO.FileInfo("$SourceDirectory\setup.exe") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
            $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

            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
        }

        if ($Config.Lock -eq $true) {
        }

        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()

            Write-Log -Message "Performing sanity checks for Exchange readiness on first run." -Level VERBOSE
            $Result = Test-ExchangeReadiness -Config $Config -Credential $Credential
        }
        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) {
                Write-Log -Message "Caught the installation running in a loop. The previous phase $($Config.PreviousPhase) is the same as the current phase." -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."
                }

                Exit 1
            }
            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 

                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
                                    Exit $ERR_PROBLEMADDINGFEATURE
                                }
                            }
                        }
                        catch [Exception] {
                            Write-Log -Message "Error installing windows features." -Level ERROR -ErrorRecord $_
                            Exit 1
                        }

                        Write-Log -Message "Completed Operating System prerequisites."

                        $Config.Phase++

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

                        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/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/Windows8.1-KB3146715-x64.msu"
                                        $PackageId = "KB3146715"
                                        $Arguments = @("/install", "/quiet", "/norestart")
                                        break
                                    }
                                    $WS2012_MAJOR {
                                        $Url = "http://download.microsoft.com/download/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/Windows8-RT-KB3146714-x64.msu"
                                        $PackageId = "KB3146714"
                                        $Arguments = @("/install", "/quiet", "/norestart")
                                        break
                                    }
                                    $WS2008R2_MAJOR {
                                        $Url = "http://download.microsoft.com/download/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/NDP461-KB3146716-x86-x64-ENU.exe"
                                        $PackageId = "KB3146716"
                                        $Arguments =  @("/q", "/norestart")
                                        break
                                    }
                                    default {
                                        Write-Log -Message "Unknown OS version $MajorOSVersion." -Level ERROR
                                        $Url = [System.String]::Empty
                                        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 $_
                                    }
                                }
                            }
                        }
                    
                        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
                                    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 = @()
                                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
                        }

                        $Config.Phase++

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

                        $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."

                        $Config.Phase++

                        break
                    }
                    4 {
                        Write-Log -Message "*** PHASE 4 *** : Installing Exchange."
                        $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."

                        $Config.Phase++

                        break
                    }
                    5 {
                        Write-Log -Message "*** PHASE 5 *** : Post configuration tasks."
                        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 (!$Success)
                                {
                                    Write-Log -Message "Could not add the Exchange Trusted Subsystem to the witness server local administrators group. Cannot complete DAG setup." -Level ERROR
                                    Exit 1
                                }
                            }
                        }

                        Write-Log -Message "Completed post-configuration tasks."

                        $Config.Phase++

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

                        if (Get-Service -Name MSExchangeTransport -ErrorAction SilentlyContinue) {
                            Write-Log -Message "Configuring MSExchangeTransport startup to Automatic."
                            Set-Service MSExchangeTransport -StartupType Automatic
                            try {
                                Start-Service -Name MSExchangeTransport
                            }
                            catch [Exception] {
                                Write-Log -Message "Error starting MSExchangeTransport." -Level ERROR -ErrorRecord $_
                            }
                        }
                        if (Get-Service -Name MSExchangeFrontEndTransport -ErrorAction SilentlyContinue) {
                            Write-Log -Message "Configuring MSExchangeFrontEndTransport startup to Automatic."
                            Set-Service MSExchangeFrontEndTransport -StartupType Automatic
                            try {
                                Start-Service -Name MSExchangeFrontEndTransport
                            }
                            catch [Exception] {
                                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
                                    {
                                        Write-Log -Message "The Exchange Trusted Subsystem could not be added to the local administrators group." -Level ERROR
                                        Exit 1
                                    }
                                }
                                catch [Exception] {
                                    Write-Log -Message "Error adding server to DAG." -Level ERROR -ErrorRecord $_
                                }
                            }
                            catch [Exception] {
                                Write-Log -Message "Error creating DAG $($Config.DAGName)." -Level ERROR -ErrorRecord $_
                            }
                        }

                        Write-Log -Message "Setup finished."

                        $Config.Phase++

                        break
                    }
                    default {
                        Write-Log -Message "Unknown phase $($Config.Phase)." -Level ERROR
                        break
                    }
                }

                Set-OpenFileSecurityWarning -Enable 

                if ($Config.Unattended -eq $true ) {
                    #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

                        if ($Config.FirstRun -eq $true) {
                            if ($Credential -ne $null -and $Credential -ne [PSCredential]::Empty) {
                                
                                <# Shouldn't need to use Invoke-Command
                                    $Task = Invoke-Command -ComputerName $ENV:COMPUTERNAME -ScriptBlock {
                                        Write-Output (New-InstallExchangeScheduledTask -Credential (New-Object -TypeName System.Management.Automation.PSCredential($args[0], $args[1])) -ConfigFilePath $args[2] -TaskName "$($args[3])")
                                    } -Credential $Credential -ArgumentList @($Credential.UserName, $Credential.Password, $ConfigFile)
                                #>


                                $Task = New-InstallExchangeScheduledTask -Credential $Credential -ConfigFilePath "$ConfigFile" -TaskName "$($Config.TaskName)"

                            }
                            else {
                                Write-Log -Message "Unattended specified, this is a first run, but no Credential value was provided." -Level ERROR
                                Exit 1
                            }

                            $Config.FirstRun = $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 $_
                            Exit $ERR_PROBLEMSAVECONIFG
                        }
                    
                        Write-Log -Message "Rebooting in $COUNTDOWN_TIMER seconds..."
                        Start-Sleep -Seconds $COUNTDOWN_TIMER
                        Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile
                        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."
                    }
                }
            }
        }
    }

    End {
        Exit $ERR_OK
    }
}

#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)]
        [PSCredential]$Credential,
        [Parameter(Mandatory=$true)]
        [System.String]$ConfigFilePath,
        [Parameter()]
        [System.String]$TaskName = $script:InstallExchangeTaskName

    )

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

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

    Process {
        $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 
    }

    End {
        Write-Output $ScheduledTask
    }
}

Function Convert-SecureStringToString {
    <#
        .SYNOPSIS
            The cmdlet converts a secure string to standard string.
 
        .DESCRIPTION
            The cmdlet converts a secure string to standard string.
 
        .PARAMETER SecureString
            The secure string to convert to a standard string
 
        .INPUTS
            System.Security.SecureString
         
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String "test" -AsPlainText -Force)
 
            Converts the secure string created from the text "test" back to plain text.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [SecureString]$SecureString
    )

    Begin {}

    Process {
        $Marshal = [System.Runtime.InteropServices.Marshal]   
        $Password = [System.String]::Empty
        try 
        {     
            $IntPtr = $Marshal::SecureStringToBSTR($SecureString)     
            $Password = $Marshal::PtrToStringAuto($IntPtr)   
        }   
        finally 
        {     
            if ($IntPtr) {       
                $Marshal::ZeroFreeBSTR($IntPtr)     
            }   
        }
    }

    End {
        Write-Output $Password
    }
}

#endregion