PS.MTA-STS.psm1

function Add-PSMTASTSCustomDomain {
    <#
        .SYNOPSIS
        Add-PSMTASTSCustomDomain adds custom domains to MTA-STS Function App.
 
        .DESCRIPTION
        Add-PSMTASTSCustomDomain adds custom domains to MTA-STS Function App. It also creates new certificate for each domain and adds binding to Function App.
 
        .PARAMETER CsvPath
        Provide path to csv file with accepted domains. Csv file should have one column with header "DomainName" and list of domains in each row.
 
        .PARAMETER DomainName
        Provide list of domains.
 
        .PARAMETER ResourceGroupName
        Provide name of Resource Group where Function App is located.
 
        .PARAMETER FunctionAppName
        Provide name of Function App.
 
        .PARAMETER DoNotAddManagedCertificate
        Switch to not add managed certificate to Function App. This is useful, if you want to use your own certificate.
 
        .PARAMETER CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER WhatIf
        Switch to run the command in a WhatIf mode.
 
        .PARAMETER Confirm
        Switch to run the command in a Confirm mode.
 
        .PARAMETER Verbose
        Switch to run the command in a Verbose mode.
 
        .EXAMPLE
        Add-PSMTASTSCustomDomain -CsvPath "C:\temp\accepted-domains.csv" -ResourceGroupName "MTA-STS" -FunctionAppName "MTA-STS-FunctionApp"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and adds them to Function App "MTA-STS-FunctionApp" in Resource Group "MTA-STS". It also creates new certificate for each domain and adds binding to Function App.
 
        .EXAMPLE
        Add-PSMTASTSCustomDomain -DomainName "contoso.com", "fabrikam.com" -ResourceGroupName "MTA-STS" -FunctionAppName "MTA-STS-FunctionApp"
 
        Adds domains "contoso.com" and "fabrikam.com" to Function App "MTA-STS-FunctionApp" in Resource Group "MTA-STS". It also creates new certificate for each domain and adds binding to Function App.
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Csv")]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Csv")]
        [string]
        $CsvPath,

        [Parameter(Mandatory = $true, ParameterSetName = "Manual")]
        [string[]]
        $DomainName,

        [Parameter(Mandatory = $true)]
        [string]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]
        $FunctionAppName,

        [switch]
        $DoNotAddManagedCertificate,

        [Parameter(ParameterSetName = "Csv")]
        [string]
        $CsvEncoding = "UTF8",

        [Parameter(ParameterSetName = "Csv")]
        [string]
        $CsvDelimiter = ";"
    )
    
    begin {
        if ($null -eq (Get-AzContext)) {
            Write-Warning "Connecting to Azure service..."
            $null = Connect-AzAccount -ErrorAction Stop
        }

        if ($CsvPath) {
            # Import csv file with accepted domains
            Write-Verbose "Importing csv file from $CsvPath..."
            $domainList = Import-Csv -Path $CsvPath -Encoding $CsvEncoding -Delimiter $CsvDelimiter -ErrorAction Stop
        }

        if ($DomainName) {
            $domainList = @()
            foreach ($domain in $DomainName) { $domainList += @{DomainName = $domain } }
        }

        # Prepare new domains
        $newCustomDomains = @()
        foreach ($domain in $domainList) {
            # Check, if domain has correct format
            if ($domain.DomainName -notmatch "^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$") {
                Write-Error -Message "Domain $($domain.DomainName) has incorrect format. Please provide domain in format 'contoso.com'."
                return
            }

            # Prepare new domain
            if ($domain.DomainName -notlike "mta-sts.*") {
                Write-Verbose "Adding prefix 'mta-sts.' to domain $($domain.DomainName)..."
                $domain.DomainName = "mta-sts.$($domain.DomainName)"
            }

            # Add new domain to list of domains
            if ($domain.DomainName -notin $newCustomDomains) {
                Write-Verbose "Adding domain $($domain.DomainName) to list of domains..."
                $newCustomDomains += $domain.DomainName
            }
        }

        # Check, if a domain name is already used in our Function App
        $currentHostnames = Get-PSMTASTSCustomDomain -ResourceGroupName $ResourceGroupName -FunctionAppName $FunctionAppName -ErrorAction Stop
        $customDomainsToAdd = @()
        foreach ($newDomain in $newCustomDomains) {
            if ($newDomain -in $currentHostnames) {
                Write-Verbose "Domain $newDomain already exists in Function App $FunctionAppName. Skipping..."
                continue
            }

            Write-Verbose "Adding domain $newDomain to list of domains, which should be added to Function App $FunctionAppName..."
            $customDomainsToAdd += $newDomain
        }

        # Check, if there are new domains to add
        if ($customDomainsToAdd.count -eq 0) {
            Write-Verbose "No new domains to add to Function App $FunctionAppName."
            return
        }

        # Add the current domains to the list of new domains
        $newCustomDomains = $currentHostnames + $customDomainsToAdd
    }
    
    process {
        # Add new domains to Function App
        Write-Verbose "Adding $($customDomainsToAdd.count) domains to Function App $FunctionAppName : $($customDomainsToAdd -join ", ")..."
        $setAzWebApp = @{
            ResourceGroupName = $ResourceGroupName
            Name              = $FunctionAppName
            HostNames         = $newCustomDomains
            ErrorAction       = "Stop"
            WarningAction     = "Stop"
        }

        try {
            if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Add custom domains")) {
                $null = Set-AzWebApp @setAzWebApp
            }
        }
        catch {
            if ($_.Exception.Message -like "*A TXT record pointing from*was not found*") {
                Write-Warning -Message $_.Exception.Message
            }
            else {
                Write-Error -Message $_.Exception.Message
                return
            }
        }

        # Stop here, if we should not add managed certificate
        if ($DoNotAddManagedCertificate) {
            Write-Verbose "Managed certificate will not be added to Function App $FunctionAppName."
            return
        }

        # Add managed certificate to Function App
        foreach ($customDomainToAdd in $customDomainsToAdd) {
            Write-Verbose "Adding certificate for $customDomainToAdd..."
            $newAzWebAppCertificate = @{
                ResourceGroupName = $ResourceGroupName
                WebAppName        = $FunctionAppName
                Name              = "mtasts-cert-$($customDomainToAdd.replace(".", "-"))"
                HostName          = $customDomainToAdd
                AddBinding        = $true
                SslState          = "SniEnabled"
            }

            try {
                if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Add certificate for $customDomainToAdd")) {
                    $null = New-AzWebAppCertificate @newAzWebAppCertificate
                }
            }
            catch {
                Write-Error -Message $_.Exception.Message
                return
            }
        }
    }
}

function Export-PSMTASTSDomainsFromExo {
    <#
        .SYNOPSIS
        Export-PSMTASTSDomainsFromExo.ps1
 
        .DESCRIPTION
        This script exports all domains from Exchange Online and checks, if the MX record points to Exchange Online. The result is exported to a .csv file.
 
        .PARAMETER DisplayResult
        Provide a Boolean value, if the result should be displayed in the console. Default is $true.
 
        .PARAMETER CsvPath
        Provide a String containing the path to the .csv file, where the result should be exported to.
 
        .PARAMETER MTASTSDomain
        Provide a PSCustomObject containing the result of Get-AcceptedDomain. If not provided, the script will run Get-AcceptedDomain itself.
 
        .PARAMETER DnsServerToQuery
        Provide a String containing the IP address of the DNS server, which should be used to query the MX record. Default is 8.8.8.8 (Google DNS).
 
        .PARAMETER ExoHost
        Provide a String containing the host name of the MX record, which should be used to check, if the MX record points to Exchange Online. Default is *.mail.protection.outlook.com.
 
        .PARAMETER CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER Verbose
        Switch to run the command in a Verbose mode.
 
        .EXAMPLE
        Export-PSMTASTSDomainsFromExo.ps1 -CsvPath "C:\Temp\MTASTSDomains.csv"
 
        Gets accepted domains from Exchange Online and checks, if the MX record points to Exchange Online. The result is exported to "C:\Temp\MTASTSDomains.csv".
 
        .EXAMPLE
        Get-AcceptedDomain -ResultSize 10 | Export-PSMTASTSDomainsFromExo.ps1 -CsvPath "C:\Temp\MTASTSDomains.csv"
 
        Gets 10 accepted domains from Exchange Online and checks, if the MX record points to Exchange Online. The result is exported to "C:\Temp\MTASTSDomains.csv".
        If you want to filter the accepted domains first, you can do so and pipe it to the Export-PSMTASTSDomainsFromExo function.
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $CsvPath,

        [Parameter(ValueFromPipeline = $true)]
        [PSObject[]]
        $MTASTSDomain,

        [Bool]
        $DisplayResult = $true,

        [String]
        $DnsServerToQuery = "8.8.8.8",

        [string]
        $CsvEncoding = "UTF8",

        [string]
        $CsvDelimiter = ";",

        [Parameter(DontShow = $true)]
        [String]
        $ExoHost = "*.mail.protection.outlook.com"
    )
    
    begin {
        # Get all domains from Exchange Online
        $result = @()
    }
    
    process {
        trap {
            Write-Error $_
            return
        }
        
        # Connect to Exchange Online, if not already connected
        $exchangeConnection = Get-ConnectionInformation -ErrorAction SilentlyContinue | Sort-Object -Property TokenExpiryTimeUTC -Descending | Select-Object -First 1 -ExpandProperty State
        if (($exchangeConnection -ne "Connected") -and ($null -eq $MTASTSDomain)) {
            Write-Warning "Connecting to Exchange Online..."
            $null = Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
        }

        if($null -eq $MTASTSDomain) {
            Write-Verbose "Getting all domains from Exchange Online and checking MX-Record. Please wait..."
            $MTASTSDomain = Get-AcceptedDomain -ResultSize unlimited | Sort-Object -Property Name
        }

        foreach ($mtastsd in $MTASTSDomain) {
        
            $resultObject = [PSCustomObject]@{
                Name                  = $mtastsd.Name
                DomainName            = $mtastsd.DomainName
                MTA_STS_CanBeUsed     = ""
                MX_Record_Pointing_To = ""
            }
        
            Write-Verbose "Checking MX record for $($mtastsd.DomainName)..."
            $mxRecord = Resolve-DnsName -Name $mtastsd.DomainName -Type MX -Server $DnsServerToQuery -ErrorAction SilentlyContinue
            if ($mtastsd.DomainName -like "*.onmicrosoft.com") {
                $resultObject.MX_Record_Pointing_To = "WARNING: You cannot configure MTA-STS for an onmicrosoft.com domain."
                $resultObject.MTA_STS_CanBeUsed = "No"
            }
            elseif (($mxRecord.NameExchange.count -eq 1) -and ($mxRecord.NameExchange -like $ExoHost)) {
                $resultObject.MX_Record_Pointing_To = $mxRecord.NameExchange
                $resultObject.MTA_STS_CanBeUsed = "Yes"
            }
            elseif (($mxRecord.NameExchange.count -gt 1) -or ($mxRecord.NameExchange -notlike $ExoHost)) {
                $resultObject.MX_Record_Pointing_To = "WARNING: MX Record does not point to Exchange Online (only). The following host(s) was/were found: $($mxRecord.NameExchange -join ", ")"
                $resultObject.MTA_STS_CanBeUsed = "No"
            }
            else {
                $resultObject.MX_Record_Pointing_To = "ERROR: No MX record found. Please assure, that the MX record for $($mtastsd.DomainName) points to Exchange Online."
                $resultObject.MTA_STS_CanBeUsed = "No"
            }
        
            $result += $resultObject
        }
    }
    
    end {
        # Output the result in a new PowerShell window
        $domainsToExport = $result
        if($DisplayResult) {
            Write-Warning "Please select/highlight the domains you want to configure MTA-STS for in the new PowerShell window and click OK. You can select multiple entries by holding the CTRL key OR by selecting your first entry, then holding the SHIFT key and selecting your last entry."
            $domainsToExport = $result | Sort-Object -Property MTA_STS_CanBeUsed, Name | Out-GridView -Title "Please select the domains you want to use for MTA-STS and click OK." -PassThru
        }

        # Check if the user selected any domains
        if ($null -eq $domainsToExport) {
            Write-Verbose "No domains selected. Exiting."
        }

        # Export the result to a .csv file
        Write-Verbose "Exporting $($domainsToExport.count) domain(s) to $CsvPath..."
        $domainsToExport | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding $CsvEncoding -Delimiter $CsvDelimiter -Force
    }
}

function Get-PSMTASTSCustomDomain {
    <#
        .SYNOPSIS
        Get-PSMTASTSCustomDomain gets custom domains of MTA-STS Function App.
 
        .DESCRIPTION
        Get-PSMTASTSCustomDomain gets custom domains of MTA-STS Function App.
 
        .PARAMETER ResourceGroupName
        Provide name of Resource Group where Function App is located.
 
        .PARAMETER FunctionAppName
        Provide name of Function App.
 
        .EXAMPLE
        Get-PSMTASTSCustomDomain -ResourceGroupName "MTA-STS" -FunctionAppName "MTA-STS-FunctionApp"
 
        Gets list of custom domains of Function App "MTA-STS-FunctionApp" in Resource Group "MTA-STS".
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]
        $FunctionAppName
    )
    
    begin {
        if ( -not (Get-AzContext)) {
            Write-Warning "Connecting to Azure service..."
            $null = Connect-AzAccount -ErrorAction Stop
        }
    }
    
    process {
        Write-Verbose "Getting domains of Function App $FunctionAppName..."
        Get-AzWebApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -ErrorAction Stop | Select-Object -ExpandProperty HostNames
    }
}

function New-PSMTASTSFunctionAppDeployment {
    <#
    .SYNOPSIS
        Creates an Azure Function App with the needed PowerShell code to publish MTA-STS policies.
 
    .DESCRIPTION
        Creates an Azure Function App with the needed PowerShell code to publish MTA-STS policies.
        The Azure Function App will be created in the specified resource group and location.
        If the resource group doesn't exist, it will be created.
        If the Azure Function App doesn't exist, it will be created.
        If the Azure Function App exists, it will be updated with the latest PowerShell code. This will overwrite any changes you made to the Azure Function App!
 
    .PARAMETER Location
        Provide the Azure location, where the Azure Function App should be created.
        You can get a list of all available locations by running 'Get-AzLocation | Select-Object -ExpandProperty location | Sort-Object'
     
    .PARAMETER ResourceGroupName
        Provide the name of the Azure resource group, where the Azure Function App should be created.
        If the resource group doesn't exist, it will be created.
        If the resource group exists already, it will be used.
     
    .PARAMETER FunctionAppName
        Provide the name of the Azure Function App, which should be created.
        If the Azure Function App doesn't exist, the Azure Storace Account and Azure Function App will be created.
        If the Azure Function App exists, it will be updated with the latest PowerShell code. This will overwrite any changes you made to the Azure Function App!
         
    .PARAMETER StorageAccountName
        Provide the name of the Azure Storage Account, which should be created.
        If the Azure Function App doesn't exist, the Azure Storace Account and Azure Function App will be created.
 
    .PARAMETER WhatIf
        If this switch is provided, no changes will be made. Only a summary of the changes will be shown.
 
    .PARAMETER Confirm
        If this switch is provided, you will be asked for confirmation before any changes are made.
 
    .EXAMPLE
        New-PSMTASTSFunctionAppDeployment -Location 'West Europe' -ResourceGroupName 'rg-PSMTASTS' -FunctionAppName 'func-PSMTASTS' -StorageAccountName 'stpsmtasts'
         
        Creates an Azure Function App with the name 'PSMTASTS' in the resource group 'PSMTASTS' in the location 'West Europe'.
        If the resource group doesn't exist, it will be created.
        If the Azure Function App doesn't exist, it will be created.
        If the Azure Function App exists, it will be updated with the latest PowerShell code.
 
    .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>


    #region Parameter
    [CmdletBinding(SupportsShouldProcess)]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        $Location,

        [Parameter(Mandatory = $true)]
        [String]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [String]
        $FunctionAppName,

        [Parameter(Mandatory = $true)]
        [String]
        $StorageAccountName
    )
    #endregion Parameter

    begin {
        # Check if needed PowerShell modules are installed
        Write-Verbose "Checking if needed PowerShell modules are installed."
        $neededModules = @(
            'Az.Accounts',
            'Az.Resources',
            'Az.Websites'
        )
        $missingModules = @()
        foreach ($neededModule in $neededModules) {
            if ($null -eq (Get-Module -Name $neededModule -ListAvailable -ErrorAction SilentlyContinue)) {
                $missingModules += $neededModule
            }
        }
        if ($missingModules.Count -gt 0) {
            throw @"
The following modules are missing: '{0}'. Please install them using "Install-Module -Name '{0}'
"@
 -f ($missingModules -join "', '")
        }

        # Check if PowerShell is connected to Azure
        if ( -not (Get-AzContext)) {
            Write-Warning "Connecting to Azure service..."
            $null = Connect-AzAccount -ErrorAction Stop
        }
    }
    
    process {
        trap {
            Write-Error $_

            # Clean up, if needed
            if (Test-Path -Path $workingDirectory) {
                $null = Remove-Item -Path $workingDirectory -Recurse -Force
            }

            return
        }
        
        # Check, if Location is valid
        Write-Verbose "Checking if location '$($Location)' is valid."
        $validLocations = Get-AzLocation | Select-Object -ExpandProperty location
        if(-not ($Location -in $validLocations)) {
            Write-Verbose "Location '$($Location)' is not valid. Please provide one of the following values: $($validLocations -join ', ')"
            return
        }

        # Create, if resource group doesn't exist already. If it doesn't exist, create it.
        if ($null -eq (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) {
            Write-Verbose "Creating Azure Resource Group $($ResourceGroupName)..."
            $null = New-AzResourceGroup -Name $ResourceGroupName -Location $Location
        }

        # Set default resource group for future cmdlets in this powershell session
        $null = Set-AzDefault -ResourceGroupName $ResourceGroupName

        # Check, if FunctionApp exists already
        $functionAppCreated = $false
        if($null -eq (Get-AzWebApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -ErrorAction SilentlyContinue)) {
            # Create Storage Account
            Write-Verbose "Creating Azure Storage Account $StorageAccountName..."
            $newAzStorageAccountProps = @{
                ResourceGroupName = $ResourceGroupName
                Name = $StorageAccountName
                Location = $Location
                SkuName = 'Standard_LRS'
                AllowBlobPublicAccess = $false
                ErrorAction = 'Stop'
            }
            $null = New-AzStorageAccount @newAzStorageAccountProps

            # Create Function App
            Write-Verbose "Creating Azure Function App $FunctionAppName..."
            $newAzFunctionAppProps = @{
                ResourceGroupName = $ResourceGroupName
                Name = $FunctionAppName
                Location = $Location
                Runtime = 'PowerShell'
                StorageAccountName = $StorageAccountName
                FunctionsVersion = '4'
                OSType = 'Windows'
                RuntimeVersion = '7.2'
                ErrorAction = 'Stop'
            }
            $null = New-AzFunctionApp @newAzFunctionAppProps

            $functionAppCreated = $true
        }

        # Create Function App contents in temporary folder and zip it
        $workingDirectory = Join-Path -Path $env:TEMP -ChildPath "PS.MTA-STS_deployment"
        if (Test-Path -Path $workingDirectory) {
            $null = Remove-Item -Path $workingDirectory -Recurse -Force
        }
        $null = New-Item -Path $workingDirectory -ItemType Directory
        $null = New-Item -Path "$workingDirectory/function" -ItemType Directory
        $null = $PSMTASTS_hostJson | Set-Content -Path "$workingDirectory/function/host.json" -Force
        $null = $PSMTASTS_profilePs1 | Set-Content -Path "$workingDirectory/function/profile.ps1" -Force
        $null = $PSMTASTS_requirementsPsd1 | Set-Content -Path "$workingDirectory/function/requirements.psd1" -Force
        $null = New-Item -Path "$workingDirectory/function/Publish-MTASTSPolicy" -ItemType Directory
        $null = $PSMTASTS_functionJson | Set-Content -Path "$workingDirectory/function/Publish-MTASTSPolicy/function.json" -Force
        $null = $PSMTASTS_runPs1 | Set-Content -Path "$workingDirectory/function/Publish-MTASTSPolicy/run.ps1" -Force
        $null = Compress-Archive -Path "$workingDirectory/function/*" -DestinationPath "$workingDirectory/Function.zip" -Force

        # Wait for Function App to be ready
        if($functionAppCreated) {
            Write-Verbose "Waiting for Azure Function App $($FunctionAppName) to be ready..."
            $null = Start-Sleep -Seconds 60
        }

        # Upload PowerShell code to Azure Function App
        Write-Verbose "Uploading PowerShell code to Azure Function App $($FunctionAppName)..."
        $null = Publish-AzWebApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force
        
        # Clean up
        if (Test-Path -Path $workingDirectory) {
            $null = Remove-Item -Path $workingDirectory -Recurse -Force
        }
    }
}

function Remove-PSMTASTSCustomDomain {
    <#
        .SYNOPSIS
        Remove-PSMTASTSCustomDomain removes custom domains from MTA-STS Function App.
 
        .DESCRIPTION
        Remove-PSMTASTSCustomDomain removes custom domains from MTA-STS Function App. It does not remove AzWebAppCertificates, as they could be used elsewhere.
 
        .PARAMETER CsvPath
        Provide path to csv file with accepted domains. Csv file should have one column with header "DomainName" and list of domains in each row.
 
        .PARAMETER DomainName
        Provide list of domains.
 
        .PARAMETER ResourceGroupName
        Provide name of Resource Group where Function App is located.
 
        .PARAMETER FunctionAppName
        Provide name of Function App.
 
        .PARAMETER CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER WhatIf
        Switch to run the command in a WhatIf mode.
 
        .PARAMETER Confirm
        Switch to run the command in a Confirm mode.
 
        .PARAMETER Verbose
        Switch to run the command in a Verbose mode.
 
        .EXAMPLE
        Remove-PSMTASTSCustomDomain -CsvPath "C:\temp\accepted-domains.csv" -ResourceGroupName "MTA-STS" -FunctionAppName "MTA-STS-FunctionApp"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and removes them from Function App "MTA-STS-FunctionApp" in Resource Group "MTA-STS".
 
        .EXAMPLE
        Remove-PSMTASTSCustomDomain -DomainName "contoso.com", "fabrikam.com" -ResourceGroupName "MTA-STS" -FunctionAppName "MTA-STS-FunctionApp"
 
        Removes domains "contoso.com" and "fabrikam.com" from Function App "MTA-STS-FunctionApp" in Resource Group "MTA-STS".
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Csv")]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Csv")]
        [string]
        $CsvPath,

        [Parameter(Mandatory = $true, ParameterSetName = "Manual")]
        [string[]]
        $DomainName,

        [Parameter(Mandatory = $true)]
        [string]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]
        $FunctionAppName,

        [Parameter(ParameterSetName = "Csv")]
        [string]
        $CsvEncoding = "UTF8",

        [Parameter(ParameterSetName = "Csv")]
        [string]
        $CsvDelimiter = ";"
    )
    
    begin {
        if ($null -eq (Get-AzContext)) {
            Write-Warning "Connecting to Azure service..."
            $null = Connect-AzAccount -ErrorAction Stop
        }

        if ($CsvPath) {
            # Import csv file with accepted domains
            Write-Verbose "Importing csv file from $CsvPath..."
            $domainList = Import-Csv -Path $CsvPath -Encoding $CsvEncoding -Delimiter $CsvDelimiter -ErrorAction Stop
        }

        if ($DomainName) {
            $domainList = @()
            foreach ($domain in $DomainName) { $domainList += @{DomainName = $domain } }
        }

        # Prepare new domains
        $removeCustomDomains = @()
        foreach ($domain in $domainList) {
            # Check, if domain has correct format
            if ($domain.DomainName -notmatch "^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$") {
                Write-Error -Message "Domain $($domain.DomainName) has incorrect format. Please provide domain in format 'contoso.com'."
                return
            }

            # Prepare new domain
            if ($domain.DomainName -notlike "mta-sts.*") {
                Write-Verbose "Adding prefix 'mta-sts.' to domain $($domain.DomainName)..."
                $domain.DomainName = "mta-sts.$($domain.DomainName)"
            }

            # Add new domain to list of domains
            if ($domain.DomainName -notin $removeCustomDomains) {
                Write-Verbose "Adding domain $($domain.DomainName) to list of domains..."
                $removeCustomDomains += $domain.DomainName
            }
        }

        # Check, if a domain name is already used in our Function App
        $currentHostnames = Get-PSMTASTSCustomDomain -ResourceGroupName $ResourceGroupName -FunctionAppName $FunctionAppName -ErrorAction Stop
        $customDomainsToRemove = @()
        foreach ($newDomain in $removeCustomDomains) {
            if ($newDomain -notin $currentHostnames) {
                Write-Verbose "Domain $newDomain does not exists in Function App $FunctionAppName. Skipping..."
                continue
            }

            Write-Verbose "Adding domain $newDomain to list of domains, which should be removed from Function App $FunctionAppName..."
            $customDomainsToRemove += $newDomain
        }

        # Add the current domains to the list of domains to remove
        $newCustomDomains = Compare-Object -ReferenceObject $currentHostnames -DifferenceObject $customDomainsToRemove | Where-Object -FilterScript {$_.SideIndicator -eq "<="} | Select-Object -ExpandProperty InputObject
    }
    
    process {
        # Check, if there are new domains to remove
        if ($customDomainsToRemove.count -eq 0) {
            Write-Verbose "No domains to remove from Function App $FunctionAppName."
            return
        }

        # Remove domains from Function App
        Write-Verbose "Removing $($customDomainsToRemove.count) domains from Function App $FunctionAppName : $($customDomainsToRemove -join ", ")..."
        $setAzWebApp = @{
            ResourceGroupName = $ResourceGroupName
            Name              = $FunctionAppName
            HostNames         = $newCustomDomains
            ErrorAction       = "Stop"
            WarningAction     = "Stop"
        }

        try {
            if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Remove custom domains")) {
                $null = Set-AzWebApp @setAzWebApp
            }
        }
        catch {
            Write-Error -Message $_.Exception.Message
            return
        }
    }
}

function Test-PSMTASTSConfiguration {
    <#
        .SYNOPSIS
        Test-PSMTASTSConfiguration checks if MTA-STS is configured correctly for all domains in a CSV file.
 
        .DESCRIPTION
        Test-PSMTASTSConfiguration checks if MTA-STS is configured correctly for all domains in a CSV file.
        It checks if the...
        - ...TXT record is configured correctly,
        - ...CNAME record is configured correctly,
        - ...policy file is available and
        - ...MX record is configured correctly.
 
        .PARAMETER DisplayResult
        Provide a Boolean value, if the result should be displayed in the console. Default is $true.
 
        .PARAMETER CsvPath
        Provide path to csv file with accepted domains.
        Csv file should have one column with header "DomainName" and list of domains in each row.
 
        .PARAMETER FunctionAppName
        Provide name of Function App.
 
        .PARAMETER DnsServer
        Provide IP address of DNS server to use for DNS queries. Default is 8.8.8.8 (Google DNS).
 
        .PARAMETER ExportResult
        Switch Parameter. Export result to CSV file.
         
        .PARAMETER ResultPath
        Provide path to CSV file where result should be exported. Default is "C:\temp\mta-sts-result.csv".
 
        .PARAMETER ExoHost
        Provide a String containing the host name of the MX record, which should be used to check, if the MX record points to Exchange Online. Default is *.mail.protection.outlook.com.
 
        .PARAMETER WhatIf
        Switch to run the command in a WhatIf mode.
 
        .PARAMETER Confirm
        Switch to run the command in a Confirm mode.
 
        .EXAMPLE
        Test-PSMTASTSConfiguration -CsvPath "C:\temp\accepted-domains.csv" -FunctionAppName "MTA-STS-FunctionApp"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and checks if MTA-STS is configured correctly for each domain in Function App "MTA-STS-FunctionApp".
 
        .EXAMPLE
        Test-PSMTASTSConfiguration -CsvPath "C:\temp\accepted-domains.csv" -FunctionAppName "MTA-STS-FunctionApp" -ExportResult -ResultPath "C:\temp\mta-sts-result.csv"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and checks if MTA-STS is configured correctly for each domain in Function App "MTA-STS-FunctionApp". It also exports result to "C:\temp\mta-sts-result.csv".
    #>

    [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $CsvPath,
        
        [Parameter(Mandatory = $true)]
        [String]
        $FunctionAppName,

        [String]
        $DnsServer = "8.8.8.8",

        [Bool]
        $DisplayResult = $true,

        [Parameter(ParameterSetName = "ExportResult")]
        [Switch]
        $ExportResult,
    
        [Parameter(Mandatory = $true, ParameterSetName = "ExportResult")]
        [String]
        $ResultPath,

        [Parameter(DontShow = $true)]
        [String]
        $ExoHost = "*.mail.protection.outlook.com"
    )

    process {
        trap {
            Write-Error $_
            return
        }

        # Prepare variables
        $csv = Import-Csv -Path $CsvPath -Encoding UTF8 -Delimiter ";" -ErrorAction Stop
        $txtRecordContent = "v=STSv1; id=*Z;"
        $mtaStsPolicyFileContent = @"
version: STSv1
mode: enforce
mx: $ExoHost
max_age: 604800
"@

        $counter = 1
        $result = @()

        # Loop through all domains
        foreach ($line in $csv) {
            Write-Verbose "$counter / $($csv.count) - $($line.DomainName)"

            # Prepare result object
            $resultObject = [PSCustomObject]@{
                DomainName         = $line.DomainName
                Host               = "$FunctionAppName.azurewebsites.net"
                MTA_STS_TXT        = ""
                MTA_STS_CNAME      = ""
                MTA_STS_PolicyFile = ""
                MTA_STS_MX         = ""
                MTA_STS_OVERALL    = "OK"
            }

            # Prepare MTA-STS name
            $mtaStsName = "mta-sts.$($line.DomainName)"

            # Check MTA-STS TXT record
            Write-Verbose "...Checking MTA-STS TXT record for $mtaStsName."
            $txtRecord = Resolve-DnsName -Name "_$mtaStsName" -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue
            if ($txtRecord -and ($txtRecord.strings -like $txtRecordContent)) {
                $resultObject.MTA_STS_TXT = "OK"
            }
            elseif ($txtRecord -and ($txtRecord.strings -notlike $txtRecordContent)) {
                $resultObject.MTA_STS_TXT = "TXT record does not contain the expected content. The following content was found: $($txtRecord.strings -join ", ")"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
            else {
                $resultObject.MTA_STS_TXT = "TXT record was not found. Please check if the TXT record for $mtaStsName points to the Function App $($resultObject.Host)."
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            # Check MTA-STS CNAME record
            Write-Verbose "...Checking MTA-STS CNAME record for $mtaStsName."
            $cnameRecord = Resolve-DnsName -Name $mtaStsName -Type CNAME -Server $DnsServer -ErrorAction SilentlyContinue
            if ($cnameRecord -and ($resultObject.Host -eq $cnameRecord.NameHost)) {
                $resultObject.MTA_STS_CNAME = "OK"
            }
            elseif ($cnameRecord -and ($resultObject.Host -ne $cnameRecord.NameHost)) {
                $resultObject.MTA_STS_CNAME = "CNAME record does not contain the expected content. The following content was found: $($cnameRecord.NameHost -join ", ")"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
            else {
                $resultObject.MTA_STS_CNAME = "CNAME record was not found. Please check if the CNAME record for $mtaStsName points to the Function App $($resultObject.Host)."
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
    
            # Check MTA-STS Policy File
            Write-Verbose "...Checking MTA-STS Policy File for $mtaStsName."
            $mtaStsPolicyUrl = "https://$mtaStsName/.well-known/mta-sts.txt"
            $policyFile = $null
            
            try {
                $policyFile = Invoke-WebRequest -Uri $mtaStsPolicyUrl -ErrorAction SilentlyContinue
            }
            catch {
                $resultObject.MTA_STS_PolicyFile = $_.Exception.Message
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            if ($policyFile -and ($policyFile.Content -eq $mtaStsPolicyFileContent)) {
                $resultObject.MTA_STS_PolicyFile = "OK"
            }
            if ($policyFile -and ($policyFile.Content -ne $mtaStsPolicyFileContent)) {
                $resultObject.MTA_STS_PolicyFile = "Policy file does not contain the expected content. The following content was found: $($policyFile.Content)"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            # Check MX record
            Write-Verbose "...Checking MX record for $($line.DomainName)."
            $mxRecord = Resolve-DnsName -Name $line.DomainName -Type MX -Server $DnsServer -ErrorAction SilentlyContinue
            if (($mxRecord.NameExchange.count -eq 1) -and ($mxRecord.NameExchange -like $ExoHost)) {
                $resultObject.MTA_STS_MX = "OK"
            }
            elseif (($mxRecord.NameExchange.count -ne 1) -or ($mxRecord.NameExchange -notlike $ExoHost)) {
                $resultObject.MTA_STS_MX = "MX record does not contain the expected content. The following content was found: $($mxRecord.NameExchange -join ", ")"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
            else {
                $resultObject.MTA_STS_MX = "MX record was not found. Please check if the MX record for $($line.DomainName) points to Exchange Online."
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            $result += $resultObject
            $counter++
        }

        # Output the result in a new PowerShell window
        if($DisplayResult) {
            Write-Warning "Please check the results in the new PowerShell window."
            $result | Out-GridView -Title "Test MTA-STS Configuration"
        }

        # Export result to CSV
        if ($ExportResult) {
            $result | Export-Csv -Path $ResultPath -Encoding UTF8 -Delimiter ";" -NoTypeInformation
        }
    }
}

$script:PSMTASTS_hostJson = @'
{
    "version": "2.0",
    "extensions": {
        "http": {
        "routePrefix": ""
        }
    },
    "managedDependency": {
        "Enabled": false
    },
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[3.*, 4.0.0)"
    }
    }
'@


$script:PSMTASTS_profilePs1 = @'
# Azure Functions profile.ps1
#
# This profile.ps1 will get executed every "cold start" of your Function App.
# "cold start" occurs when:
#
# * A Function App starts up for the very first time
# * A Function App starts up after being de-allocated due to inactivity
#
# You can define helper functions, run commands, or specify environment variables
# NOTE: any variables defined that are not environment variables will get reset after the first execution
# Authenticate with Azure PowerShell using MSI.
# Remove this if you are not planning on using MSI or Azure PowerShell.
 
#if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) {
# Write-Host "Connecting to Azure"
# Connect-AzAccount -Identity
#}
 
# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell.
# Enable-AzureRmAlias
# You can also define functions or aliases that can be referenced in any of your PowerShell functions.
'@


$script:PSMTASTS_requirementsPsd1 = @'
# This file enables modules to be automatically managed by the Functions service.
# See https://aka.ms/functionsmanageddependency for additional information.
#
@{
 
}
'@


$script:PSMTASTS_functionJson = @'
{
    "bindings": [
        {
        "name": "Request",
        "route": ".well-known/mta-sts.txt",
        "authLevel": "anonymous",
        "methods": [
            "get"
        ],
        "direction": "in",
        "type": "httpTrigger"
        },
        {
        "type": "http",
        "direction": "out",
        "name": "Response"
        }
    ]
}
'@


$script:PSMTASTS_runPs1 = @'
param (
 $Request,
 
 $TriggerMetadata
)
 
Write-Host "Trigger: MTA-STS policy has been requested."
 
# Prepare the response body
# Replace 'enforce' with 'testing' to test the policy without enforcing it
$PSMTASTS_mtaStsPolicy = @"
version: STSv1
mode: enforce
mx: *.mail.protection.outlook.com
max_age: 604800
"@
 
# Return the response
try {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [System.Net.HttpStatusCode]::OK
        Headers = @{
            "Content-type" = "text/plain"
        }
        Body = $PSMTASTS_mtaStsPolicy
    })
}
catch {
 # Return error, if something went wrong
 Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [System.Net.HttpStatusCode]::InternalServerError
        Headers = @{
            "Content-type" = "text/plain"
        }
        Body = $_.Exception.Message
    })
}
'@