PS.MTA-STS.psm1

function Resolve-PSMTASTSDnsName {
    <#
        .SYNOPSIS
        Resolve-PSMTASTSDnsName resolves DNS name using specified DNS server.
 
        .DESCRIPTION
        Resolve-PSMTASTSDnsName resolves DNS name using specified DNS server.
         
        .PARAMETER Name
        Name of the DNS record to resolve.
 
        .PARAMETER Type
        Type of the DNS record to resolve.
 
        .PARAMETER Server
        DNS server to use for resolving the DNS record.
 
        .EXAMPLE
        Resolve-PSMTASTSDnsName -Name "example.com" -Type "TXT" -Server "8.8.8.8"
 
        Resolves the TXT record for example.com using the DNS server "8.8.8.8". The return value is the TXT record for example.com.
    #>

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

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

        [Parameter(Mandatory = $true)]
        [String]
        $Server
    )

    Write-Verbose -Message "...checking $($Type.ToUpper()) record for $Name using DNS server $Server"
    try {
        Resolve-DnsName -Name $Name -Type $Type -Server $Server -QuickTimeout -ErrorAction Stop
    }
    catch {
        if($_.CategoryInfo.Category -eq "ResourceUnavailable") {
            Write-Verbose -Message "DNS record not found. Continuing..."
        }
        elseif($_.CategoryInfo.Category -eq "OperationTimeout") {
            throw "ERROR: Timeout period expired. Please check the DNS server and try again. Are you able to resolve the DNS record using the specified DNS server?"
        }
        else {
            throw $_
        }
    }
}

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 SkipCAACheck
        Switch to skip the Certification Authority Authorization (CAA) check.
 
        .PARAMETER CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER DnsServer
        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 WhatIf
        Switch to run the command in a WhatIf mode.
 
        .PARAMETER Confirm
        Switch to run the command in a Confirm mode.
 
        .EXAMPLE
        Add-PSMTASTSCustomDomain -CsvPath "C:\temp\accepted-domains.csv" -ResourceGroupName "MTA-STS" -FunctionAppName "func-MTA-STS"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and adds them to Function App "func-MTA-STS" 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 "func-MTA-STS"
 
        Adds domains "contoso.com" and "fabrikam.com" to Function App "func-MTA-STS" 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,

        [switch]
        $SkipCAACheck,

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

        [Parameter(ParameterSetName = "Csv")]
        [string]
        $CsvDelimiter = ";",

        [String]
        $DnsServer = "8.8.8.8"
    )
    
    begin {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Check, if we are connected to Azure
        if ($null -eq (Get-AzContext)) {
            Write-Verbose "Connecting to Azure service"
            $null = Connect-AzAccount
        }

        # Import csv file with accepted domains
        if ($CsvPath) {
            Write-Verbose "Importing csv file from $CsvPath"
            $domainList = Import-Csv -Path $CsvPath -Encoding $CsvEncoding -Delimiter $CsvDelimiter | Select-Object -ExpandProperty DomainName
        }

        # Import domains from input parameter
        if ($DomainName) {
            Write-Verbose "Creating array of domains from input parameter"
            $domainList = @()
            foreach ($domain in $DomainName) { $domainList += $domain }
        }

        # Check, if domains have correct format
        foreach ($domain in $domainList) {
            if ($domain -notmatch "^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$") {
                throw "Domain $domain has incorrect format. Please provide domain in format 'contoso.com'."
            }
        }
    }

    process {
        #Check FunctionApp
        Write-Verbose "Get Azure Function App $FunctionAppName in Resource Group $ResourceGroupName"
        $FunctionAppResult = Get-AzFunctionApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
        if ($null -eq $FunctionAppResult) {
            #Function App not found
            throw "FunctionApp $FunctionAppName not found"
        }

        #Get CustomDomain Names
        Write-Verbose "Get CustomDomainNames from Azure Function App $FunctionAppName"
        $customDomainNames = Get-PSMTASTSCustomDomain -ResourceGroupName $ResourceGroupName -FunctionAppName $FunctionAppName

        # Prepare new domains
        $newCustomDomainsToAdd = @()
        foreach ($domain in $domainList) {
            Write-Verbose "Working on Domain: $Domain"

            #Check if mta-sts.domain.tld CNAME $FunctionAppName.azurewebsites.net exists
            $mtaStsDomain = "mta-sts." + $domain
            $mtaStsCName = Resolve-PSMTASTSDnsName -Name $mtaStsDomain -Server $DnsServer -Type CNAME | Where-Object { $_.NameHost -like "*.azurewebsites.net" } | Select-Object -ExpandProperty NameHost

            # Check, if CNAME record exists
            if ($Null -eq $mtaStsCName -or $mtaStsCName -ne "$FunctionAppName.azurewebsites.net") {
                Write-Warning "CNAME record not found for $mtaStsDomain with value $FunctionAppName.azurewebsites.net. Found value: $mtaStsCName - Please create/update it and try again with this domain. Continuing with next domain."
                continue
            }

            #Check if CAA record exists and contains digicert.com
            If ($SkipCAACheck -ne $true) {
                $json = Invoke-RestMethod -Uri "https://dns.google/resolve?name=$Domain&type=CAA"
                If ($Null -ne $json.Answer.Data) {
                    if (($json.Answer.Data -notcontains '0 issue "digicert.com"') -eq $True) {
                        Write-Warning "Missing digicert.com in Certification Authority Authorization (CAA) Record for $Domain"
                        continue
                    }
                }
            }

            # Add domain to list, if it is not already added
            if ($mtaStsDomain -notin $customDomainNames.CustomDomains) {
                $newCustomDomainsToAdd += $mtaStsDomain
            }
        }

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

        # Add new domains to Function App
        $customDomainsToSet = @()
        $customDomainsToSet += $customDomainNames.DefaultDomain # Add default domain
        $customDomainsToSet += $newCustomDomainsToAdd # Add new custom domains
        if ($null -ne $customDomainNames.CustomDomains) {
            $customDomainsToSet += $customDomainNames.CustomDomains # Add existing custom domains, if any exist
        }
        Write-Verbose "Setting $($customDomainsToSet.count) domains for Function App $FunctionAppName : $($customDomainsToSet -join ", ")"
        $setAzWebApp = @{
            ResourceGroupName = $ResourceGroupName
            Name              = $FunctionAppName
            HostNames         = $customDomainsToSet
        }
        if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Add custom domains")) {
            $null = Set-AzWebApp @setAzWebApp -WarningAction SilentlyContinue
        }

        # 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
        Write-Verbose "Add Managed Certificate to Function App"
        foreach ($newCustomDomainToAdd in $newCustomDomainsToAdd) {
            Write-Verbose "Adding certificate for $newCustomDomainToAdd"
            $newAzWebAppCertificate = @{
                ResourceGroupName = $ResourceGroupName
                WebAppName        = $FunctionAppName
                Name              = "mtasts-cert-$($newCustomDomainToAdd.replace(".", "-"))"
                HostName          = $newCustomDomainToAdd
                AddBinding        = $true
                SslState          = "SniEnabled"
            }
            if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Add certificate for $newCustomDomainToAdd")) {
                $null = New-AzWebAppCertificate @newAzWebAppCertificate -WarningAction SilentlyContinue
            }
        }
    }
}

function Export-PSMTASTSDomainsFromExo {
    <#
        .SYNOPSIS
        Export-PSMTASTSDomainsFromExo
 
        .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 DomainName
        Provide a PSCustomObject containing the result of Get-AcceptedDomain. If not provided, the script will run Get-AcceptedDomain itself.
 
        .PARAMETER DnsServer
        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.
        If you have certain domains pointing to another mx endpoint than *.mail.protection.outlook.com, you can provide the host name here.
 
        .PARAMETER CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER DnsServer
        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 SkipDNSTests
        With this Parameter the DNS Checks will be skipped
 
        .EXAMPLE
        Export-PSMTASTSDomainsFromExo -CsvPath "C:\Temp\ExoDomains.csv"
 
        Gets accepted domains from Exchange Online and checks, if the MX record points to Exchange Online. The result is exported to "C:\Temp\ExoDomains.csv".
 
        .EXAMPLE
        Get-AcceptedDomain -ResultSize 10 | Export-PSMTASTSDomainsFromExo -CsvPath "C:\Temp\ExoDomains.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\ExoDomains.csv".
        If you want to filter the accepted domains first, you can do so and pipe it to the Export-PSMTASTSDomainsFromExo function.
 
        .EXAMPLE
        "contoso.com","fabrikam.com" | Export-PSMTASTSDomainsFromExo -CsvPath "C:\Temp\ExoDomains.csv"
 
        Checks if the MX record points to Exchange Online for the domains "contoso.com" and "fabrikam.com". The result is exported to "C:\Temp\ExoDomains.csv".
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

    [CmdletBinding()]
    param(
        [string]
        $CsvPath = (Join-Path -Path $env:TEMP -ChildPath "$(Get-Date -Format yyyyMMddhhmmss)_mta-sts-export.csv"),

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

        [Bool]
        $DisplayResult = $true,

        [String]
        $DnsServer = "8.8.8.8",

        [string]
        $CsvEncoding = "UTF8",

        [string]
        $CsvDelimiter = ";",

        [ValidateScript({
                # Domain Names cannot start with a dot, so we need to check for that
                # Additionally, they can start with a wildcard, if subdomains should be included
                if ($hostname -notmatch "^[a-zA-Z0-9][a-zA-Z0-9-_.]*$" -and $hostname -notmatch "^\*.[a-zA-Z0-9][a-zA-Z0-9-_.]*$") {
                    throw "MX endpoint '$hostname' is not valid. The name can contain only letters, numbers, hyphens, underscores and periods. It cannot start with a dot."
                }

                if ($hostname -like "*mail.protection.outlook.com") {
                    throw "MX endpoint '$hostname' cannot be added, because the DNS zone '*.mail.protection.outlook.com' will be added automatically. Please remove it from the list."
                }
                # if the provided hostname is valid, return $true
                $true
            })]
        [String]
        $ExoHost = "*.mail.protection.outlook.com",

        #Skip DNS Tests
        [switch]
        $SkipDNSTests
    )
    
    begin {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Prepare result array
        $result = @()
    }
    
    process {
        # 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 $DomainName)) {
            Write-Verbose "Connecting to Exchange Online"
            $null = Connect-ExchangeOnline -ShowBanner:$false
        }

        # Get either full list of accepted domains or the provided list of accepted domains
        if ($null -eq $DomainName) {
            Write-Verbose "Getting all domains from Exchange Online and checking MX-Record. Please wait..."
            $acceptedDomains = Get-AcceptedDomain -ResultSize unlimited
        }
        else {
            Write-Verbose "Checking MX-Record for provided domains. Please wait..."
            $acceptedDomains = $DomainName | Get-AcceptedDomain
        }
        $acceptedDomains = $acceptedDomains | Sort-Object -Property Name
        $AcceptedDomainsCount = $acceptedDomains.Count

        $counter = 1
        foreach ($acceptedDomain in $acceptedDomains) {
            Write-Verbose "Checking $counter / $($acceptedDomains.count) - $($acceptedDomain.DomainName)"

            #Progress Bar
            [int]$Percent = 100 / $AcceptedDomainsCount * $counter
            Write-Progress -Activity "Export in Progress" -Status "$Percent% Complete:" -PercentComplete $Percent

            # Prepare result object
            $resultObject = [PSCustomObject]@{
                Name                  = $acceptedDomain.Name        # Name of the domain
                DomainName            = $acceptedDomain.DomainName  # Domain name
                MX_Record_Pointing_To = ""                          # MX Record pointing to Exchange Online or other host(s) found? (Yes/No)
                MTA_STS_TXTRecord     = ""                          # MTA-STS TXT Record
                MTA_STS_Policy        = ""                          # MTA-STS Policy, if available
                # MTA_STS_CanBeUsed = "" # Can MTA-STS be used? (Yes/No)
                TLSRPT                = ""                          # TLSRPT Record, if available
            }

            if ($SkipDNSTests -eq $true) {
                # skip DNS checks
                Write-Verbose -Message "Skipping DNS checks"

                # Add the result to the result array
                Write-Verbose "...adding result to result array."
                $result += $resultObject

                # Increase the counter for verbose output
                $counter++

                # Continue with the next domain
                continue
            }

            # Checking MTA-STS TXT Record
            # Example: _mta-sts.example.com. IN TXT "v=STSv1; id=20160831085700Z"
            $mtaStsDNSHost = "_mta-sts." + $acceptedDomain.DomainName
            $mtaStsTXTRecord = Resolve-PSMTASTSDnsName -Name $mtaStsDNSHost -Type TXT -Server $DnsServer | Where-Object { $_.Strings -match "v=STSv1" }

            if ($null -ne $mtaStsTXTRecord) {
                $resultObject.MTA_STS_TXTRecord = $mtaStsTXTRecord.strings -join " | " # Strings are joined, because multiple strings can be returned
            }

            # Checking MTA-STS Policy
            <# Example:
                    version: STSv1
                    mode: enforce
                    mx: *.mail.protection.outlook.com
                    max_age: 604800
                #>

            $mtaStsUri = "https://mta-sts.{0}/.well-known/mta-sts.txt" -f $acceptedDomain.DomainName
            Write-Verbose "...checking MTA-STS Policy file at $mtaStsUri"
            try {
                # Try to get the MTA-STS Policy file
                $mtaStsPolicyResponse = Invoke-WebRequest -Uri $mtaStsUri -TimeoutSec 20 -ErrorAction Stop
                $resultObject.MTA_STS_Policy = ($mtaStsPolicyResponse.Content).Trim() #.Replace("`r`n","")
            }
            catch {
                # If the MTA-STS Policy file is not available or other issues occur, the result will be set to "ERROR: $($_.Exception.InnerException.Message)"
                $resultObject.MTA_STS_Policy = "ERROR: $($_.Exception.InnerException.Message)"
            }

            # Checking TLSRPT Record
            # Example: _smtp._tls.example.com. IN TXT "v=TLSRPTv1;rua=mailto:reports@example.com"
            $tlsRptDNSHost = "_smtp._tls." + $acceptedDomain.DomainName
            $tlsRptRecord = Resolve-PSMTASTSDnsName -Name $tlsRptDNSHost -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue | Where-Object { $_.Strings -like "v=TLSRPTv1*" }

            if ($null -ne $tlsRptRecord) {
                $resultObject.TLSRPT = $tlsRptRecord.Strings[0]
            }

            # Checking MX Record
            # Example: example.com. IN MX 0 example-com.mail.protection.outlook.com.
            $mxRecord = Resolve-PSMTASTSDnsName -Name $acceptedDomain.DomainName -Type MX -Server $DnsServer -ErrorAction SilentlyContinue

            # $resultObject.MTA_STS_CanBeUsed = "No" # Default value
            # Check if the domain is an onmicrosoft.com domain
            if ($acceptedDomain.DomainName -like "*.onmicrosoft.com") {
                $resultObject.MX_Record_Pointing_To = "WARNING: You cannot configure MTA-STS for an onmicrosoft.com domain."
            }
            else {
                # new else block to skip MX Record check
                $resultObject.MX_Record_Pointing_To = $mxRecord.NameExchange
            }
            <# Removed the MX record check, because we cannot tell foreach domain, which MX record is correct. Previously, this was possible, as we only checked for the single Exchange Online MX record *.mail.protection.outlook.com.
            # Check if the MX record points to Exchange Online
            elseif (($mxRecord.NameExchange.count -eq 1) -and ($mxRecord.NameExchange -like $ExoHost)) {
                $resultObject.MX_Record_Pointing_To = $mxRecord.NameExchange
                    # Check if MTA-STS can be used (TXT record and Policy file are configured)
                    if (("" -eq $resultObject.MTA_STS_TXTRecord) -and ("" -eq $resultObject.MTA_STS_Policy)) {
                        $resultObject.MTA_STS_CanBeUsed = "Yes"
                    }
                    elseif (("" -ne $resultObject.MTA_STS_TXTRecord) -and ($resultObject.MTA_STS_Policy -like "ERROR: *" -or "" -eq $resultObject.MTA_STS_Policy)) {
                        $resultObject.MTA_STS_CanBeUsed = "WARNING: MTA-STS TXT record is configured, but MTA-STS Policy file is not available."
                    }
                    elseif (("" -eq $resultObject.MTA_STS_TXTRecord) -and ("" -ne $resultObject.MTA_STS_Policy)) {
                        $resultObject.MTA_STS_CanBeUsed = "INFORMATION: MTA-STS TXT record is not configured, but MTA-STS Policy file is configured."
                    }
                    else {
                        $resultObject.MTA_STS_CanBeUsed = "COMPLETED: MTA-STS TXT record and MTA-STS Policy file are configured. Please double-check the content of the MTA-STS Policy file."
                    }
            }
            # Check if the MX record points to another host than Exchange Online or if multiple MX records were found
            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 ", ")"
            }
            # Assume, that no MX record was found
            else {
                $resultObject.MX_Record_Pointing_To = "ERROR: No MX record found. Please assure, that the MX record for $($acceptedDomain.DomainName) points to Exchange Online."
            }
            #>


            # Add the result to the result array
            Write-Verbose "...adding result to result array."
            $result += $resultObject

            # Increase the counter for verbose output
            $counter++
        }
    }
    
    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 configure MTA-STS for and click OK." -PassThru
        }

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

        # Export the result to a .csv file
        Write-Warning "Exporting $($domainsToExport.count) domain(s) to $CsvPath"
        $domainsToExport | Export-Csv -Path $CsvPath -Delimiter $CsvDelimiter -Encoding $CsvEncoding -NoTypeInformation -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 "func-MTA-STS"
 
        Gets list of custom domains of Function App "func-MTA-STS" in Resource Group "MTA-STS".
 
        .EXAMPLE
        [PSCustomObject]@{
            ResourceGroupName = "rg-MTASTS001"
            FunctionAppName = "func-MTA-STS-Enforce"
        },
        [PSCustomObject]@{
            ResourceGroupName = "rg-MTASTS002"
            FunctionAppName = "func-MTA-STS-Testing"
        } | Get-PSMTASTSCustomDomain
 
        Gets list of custom domains of Function App "func-MTA-STS-Enforce" in Resource Group "rg-MTASTS001" and Function App "func-MTA-STS-Testing" in Resource Group "rg-MTASTS002".
        The result will be a list of custom domains, default domain and SSLStates of both Function Apps.
 
        .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>

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

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]
        $FunctionAppName
    )
    
    begin {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }
        
        # Check if PowerShell is connected to Azure
        if ( -not (Get-AzContext)) {
            Write-Verbose "Connecting to Azure service..."
            $null = Connect-AzAccount
        }
    }
    
    process {
        Write-Verbose "Getting domains of Function App $FunctionAppName..."
        Get-AzWebApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName | Select-Object -Property @{
            Name       = "CustomDomains"
            Expression = {
                if ($null -ne $_.HostNames) {
                    $_.HostNames | Where-Object { $_ -notlike "*.azurewebsites.net" }
                }
            }
        },
        @{
            Name       = "DefaultDomain"
            Expression = {
                if ($null -ne $_.DefaultHostName) {
                    $_.DefaultHostName
                }
            }
        },
        @{
            Name       = "SSLState"
            Expression = {
                if ($null -ne $_.HostNameSslStates) {
                    $_.HostNameSslStates | Select-Object -Property Name, SslState, Thumbprint | Sort-Object -Property Name
                }
            }
        }
    }
}

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'
 
        If the PlanName is provided, the location will be set to the location of the App Service Plan.
        The location will still be used to create the resource group.
 
    .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 RegisterResourceProvider
        Use this Parameter to Register Resource Provider 'Microsoft.Web' if it is not yet registered.
 
    .PARAMETER DisableApplicationInsights
        By default, Application Insights is enabled during the creation of the Azure Function App.
        Use this parameter to disable the creation of the application insights resource during the function app creation. No logs will be available then.
 
    .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 StorageDeleteRetentionInDays
        Provide the number of days to retain the deleted blobs. Default is 7 days.
 
    .PARAMETER PlanName
        Provide the name of the Azure App Service Plan, 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 PlanName is provided, the location will be set to the location of the App Service Plan.
        The location will still be used to create the resource group.
 
    .PARAMETER PolicyMode
        Specifies if the policy is in "Enforce" mode or "Testing" mode
 
    .PARAMETER ExoHostName
        Provide a list of hostnames, which should be included in the MTA-STS policy.
        The list of hostnames must be valid, which means that they must be valid domain names.
        Additionally, they cannot start with a wildcard because it will be added automatically.
        *.mail.protection.outlook.com will be added automatically to the list of hostnames.
 
    .PARAMETER DnsServer
        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 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' with policy mode 'Enforce'.
        If the resource group doesn't exist, it will be created.
        If the Azure Function App doesn't exist, it will be created and app files published.
 
    .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>


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

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if ($_.length -lt 1 -or $_.length -gt 90 -or $_ -notmatch "^[a-zA-Z0-9-_.]*$") {
                    throw "ResourceGroup name '$_' is not valid. The name must be between 1 and 90 characters long and can contain only letters, numbers, hyphens, underscores and periods."
                }
                else {
                    $true
                }
            })]
        [String]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if ($_.length -lt 2 -or $_.length -gt 60 -or $_ -notmatch "^[a-zA-Z0-9-]*$") {
                    throw "Function App name '$_' is not valid. The name must be between 2 and 60 characters long and can contain only letters, numbers and hyphens."
                }
                else {
                    $true
                }
            })]
        [String]
        $FunctionAppName,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if ($_.length -lt 3 -or $_.length -gt 24 -or $_ -notmatch "^[a-z0-9]*$") {
                    throw "Storage Account name '$_' is not valid. The name must be between 3 and 24 characters long and can contain only lowercase letters and numbers."
                }
                else {
                    $true
                }
            })]
        [String]
        $StorageAccountName,

        [String]
        $PlanName,

        [int]
        $StorageDeleteRetentionInDays = 7,

        [ValidateSet("Enforce", "Testing", "None")] # ValidateSet is used to limit the possible values
        [String]
        $PolicyMode = "Enforce",

        [ValidateScript({
                # the provided list of hostnames must be valid, which means that they must be valid domain names
                # Domain Names cannot start with a dot, so we need to check for that
                # Additionally, they can start with a wildcard, if subdomains should be included
                foreach ($hostname in $_) {
                    if ($hostname -notmatch "^[a-zA-Z0-9][a-zA-Z0-9-_.]*$" -and $hostname -notmatch "^\*.[a-zA-Z0-9][a-zA-Z0-9-_.]*$") {
                        throw "MX endpoint '$hostname' is not valid. The name can contain only letters, numbers, hyphens, underscores and periods. You can also start with a wildcard character to include subdomains."
                    }

                    if ($hostname -like "*mail.protection.outlook.com") {
                        throw "MX endpoint '$hostname' cannot be added, because the DNS zone '*.mail.protection.outlook.com' will be added automatically. Please remove it from the list."
                    }
                }
                # if the list of entries is valid, return $true
                $true
            })]
        [String[]]
        $ExoHostName,

        [String]
        $DnsServer = "8.8.8.8",

        #Register AZ Resource Provider
        [Switch]
        $RegisterResourceProvider,

        #Disable Application Insights
        [Switch]
        $DisableApplicationInsights
    )
    #endregion Parameter

    begin {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Check if PowerShell is connected to Azure
        if ( -not (Get-AzContext)) {
            Write-Verbose "Connecting to Azure service..."
            $null = Connect-AzAccount
        }
    }
    
    process {
        # 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)) {
            throw "Location '$($Location)' is not valid. Please provide one of the following values: $($validLocations -join ', ')"
        }

        # Create, if resource group doesn't exist already. If it doesn't exist, create it.
        Write-Verbose "Checking if ResourceGroup '$ResourceGroupName' already exists"
        if ($null -eq (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) {
            Write-Verbose "Creating Azure Resource Group '$ResourceGroupName'..."
            if ($PSCmdlet.ShouldProcess("Resource Group $ResourceGroupName", "Create")) {
                $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 PlanName is valid, if provided
        if ($PlanName) {
            $appServicePlanResult = Get-AzAppServicePlan -ResourceGroupName $ResourceGroupName -Name $PlanName -ErrorAction SilentlyContinue
            if ($null -eq $appServicePlanResult) {
                throw "App Service Plan '$PlanName' does not exist in Resource Group '$ResourceGroupName'. Please provide a valid App Service Plan."
            }
        }

        # Check Resource Provider
        $ResourceProvider = Get-AzResourceProvider
        $MicrosoftWeb = $ResourceProvider | Where-Object { $_.ProviderNamespace -eq "Microsoft.Web" }
        if ($Null -eq $MicrosoftWeb) {
            #Resource Provicer Microsoft.Web not registered
            if ($RegisterResourceProvider -eq $true) {
                Register-AzResourceProvider -ProviderNamespace Microsoft.Web
            }
            else {
                Write-Warning -Message "Azure Resource Provider 'Microsoft.Web' is not registered. Please register the Azure Resource Provider 'Microsoft.Web' manually afterwards. See https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types#register-resource-provider for more information."
            }
        }

        # Check if Storage StorageAccountName already exists
        $storageAccountDnsResult = Resolve-PSMTASTSDnsName -Name "$StorageAccountName.blob.core.windows.net" -Type A -Server $DnsServer
        if ($null -ne $storageAccountDnsResult) {
            $StorageAccounts = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName

            # Check, if Storage Account exists already
            if (($StorageAccounts.StorageAccountName.count -eq 1 -and $StorageAccounts.StorageAccountName -eq $StorageAccountName) -or ($StorageAccounts.StorageAccountName.count -gt 1 -and $StorageAccounts.StorageAccountName -contains $StorageAccountName)) {
                # Single Storage Account found
                Write-Verbose -Message "Existing Storage Account found in Resource Group"
                [bool]$ExistingStorageAccount = $true
            }
            else {
                throw "Storage Account exists somewhere else. Please provide a different name for the Storage Account."
            }
        }
        else {
            Write-Verbose -Message "Storage Account does not exist. Will be created."
            [bool]$ExistingStorageAccount = $false
        }

        # Check, if FunctionApp exists already
        $functionAppDnsResult = Resolve-PSMTASTSDnsName -Name "$FunctionAppName.azurewebsites.net" -Type A -Server $DnsServer
        if ($null -ne $functionAppDnsResult) {
            throw "Function App already exists. Please provide a different name for the Function Account."
        }

        if ($ExistingStorageAccount -ne $true) {
            # Create Storage Account
            Write-Verbose "Creating Azure Storage Account $StorageAccountName..."
            $newAzStorageAccountProps = @{
                ResourceGroupName      = $ResourceGroupName
                Name                   = $StorageAccountName
                Location               = $Location
                SkuName                = 'Standard_LRS'
                AllowBlobPublicAccess  = $false
                EnableHttpsTrafficOnly = $true
            }
            if ($PSCmdlet.ShouldProcess("Storage Account $StorageAccountName", "Create")) {
                $null = New-AzStorageAccount @newAzStorageAccountProps
            }
        }

        # Enable soft delete for blobs
        try {
            $ctx = New-AzStorageContext -StorageAccountName $StorageAccountName
            $null = Enable-AzStorageDeleteRetentionPolicy -RetentionDays $StorageDeleteRetentionInDays -Context $ctx
        }
        catch {
            Write-Warning "Could not enable soft delete for blobs in Storage Account $StorageAccountName. Please enable it manually, if needed."
        }

        # Create Function App
        Write-Verbose "Creating Azure Function App $FunctionAppName..."
        $newAzFunctionAppProps = @{
            ResourceGroupName  = $ResourceGroupName
            Name               = $FunctionAppName
            Runtime            = 'PowerShell'
            StorageAccountName = $StorageAccountName
            FunctionsVersion   = '4'
            OSType             = 'Windows'
            RuntimeVersion     = '7.4'
        }
        # If PlanName is provided, you cannot set the Location anymore.
        # Therefore, we set the PlanName and don't set the Location, if PlanName is provided.
        if ($PlanName) {
            # Add App Service Plan, if provided
            $newAzFunctionAppProps.PlanName = $PlanName
        }
        else {
            $newAzFunctionAppProps.Location = $Location
        }
        # Disable Application Insights, if provided
        if ($DisableApplicationInsights) {
            $newAzFunctionAppProps.DisableApplicationInsights = $true
        }
        # Create Function App
        if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Create")) {
            $null = New-AzFunctionApp @newAzFunctionAppProps
        }

        # Wait for Function App to be ready
        Write-Verbose "Waiting for Azure Function App $FunctionAppName to be ready..."
        $null = Start-Sleep -Seconds 60

        # Remove AzureWebJobsDashboard, as it is deprecated
        $functionAppSettingsToRemove = @("AzureWebJobsDashboard")
        $FunctionAppSettings = Get-AzFunctionAppSetting -ResourceGroupName $ResourceGroupName -Name $FunctionAppName
        foreach ($functionAppSettingToRemove in $functionAppSettingsToRemove) {
            if ($FunctionAppSettings.Keys -contains $functionAppSettingToRemove) {
                Write-Verbose "Remove $functionAppSettingToRemove App Setting from Function App $FunctionAppName..."
                Remove-AzFunctionAppSetting -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -AppSettingName $functionAppSettingToRemove -Confirm:$false -Force
            }
        }

        # Update the Azure Function App files with the latest PowerShell code
        $updateFunctionAppProps = @{
            ResourceGroupName = $ResourceGroupName
            FunctionAppName   = $FunctionAppName
            PolicyMode        = $PolicyMode
        }
        if ($ExoHostName) {
            $updateFunctionAppProps.ExoHostName = $ExoHostName
        }
        if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Update")) {
            $null = Update-PSMTASTSFunctionAppFile @updateFunctionAppProps
        }
    }
}

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.
 
        .EXAMPLE
        Remove-PSMTASTSCustomDomain -CsvPath "C:\temp\accepted-domains.csv" -ResourceGroupName "MTA-STS" -FunctionAppName "func-MTA-STS"
 
        Reads list of accepted domains from "C:\temp\accepted-domains.csv" and removes them from Function App "func-MTA-STS" in Resource Group "MTA-STS".
 
        .EXAMPLE
        Remove-PSMTASTSCustomDomain -DomainName "contoso.com", "fabrikam.com" -ResourceGroupName "MTA-STS" -FunctionAppName "func-MTA-STS"
 
        Removes domains "contoso.com" and "fabrikam.com" from Function App "func-MTA-STS" 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 {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Check, if we are connected to Azure
        if ($null -eq (Get-AzContext)) {
            Write-Warning "Connecting to Azure service"
            $null = Connect-AzAccount
        }

        # Import csv file with accepted domains
        if ($CsvPath) {
            Write-Verbose "Importing csv file from $CsvPath"
            $domainList = Import-Csv -Path $CsvPath -Encoding $CsvEncoding -Delimiter $CsvDelimiter | Select-Object -ExpandProperty DomainName
        }

        # Import domains from input parameter
        if ($DomainName) {
            Write-Verbose "Creating array of domains from input parameter"
            $domainList = @()
            foreach ($domain in $DomainName) { $domainList += $domain }
        }

        # Check, if domains have correct format
        foreach ($domain in $domainList) {
            if ($domain -notmatch "^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$") {
                throw -Message "Domain $domain has incorrect format. Please provide domain in format 'contoso.com'."
            }
        }
    }

    process {
        #Check FunctionApp
        Write-Verbose "Get Azure Function App $FunctionAppName in Resource Group $ResourceGroupName"
        $FunctionAppResult = Get-AzFunctionApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
        if ($null -eq $FunctionAppResult) {
            #Function App not found
            throw "FunctionApp $FunctionAppName not found"
        }

        #Get CustomDomain Names
        Write-Verbose "Get CustomDomainNames from Azure Function App $FunctionAppName"
        $customDomainNames = Get-PSMTASTSCustomDomain -ResourceGroupName $ResourceGroupName -FunctionAppName $FunctionAppName

        # Prepare new domains
        $removeCustomDomains = @()
        foreach ($domain in $domainList) {
            #Check if CustomDomain is added as Custom Domain
            $mtaStsDomain = "mta-sts." + $domain
            if ($mtaStsDomain -in $CustomDomainNames.CustomDomains) {
                #Custom Domain is present
                Write-Verbose "Adding Domain to Array of Domains to remove: $mtaStsDomain"
                $removeCustomDomains += $mtaStsDomain
            }
        }

        # Check, if there are new domains to remove
        if ($removeCustomDomains.count -eq 0) {
            Write-Warning "No domains to remove from Function App $FunctionAppName. Stopping here."
            return
        }

        # Add the current domains to the list of domains to remove
        $newCustomDomains = Compare-Object -ReferenceObject $CustomDomainNames.CustomDomains -DifferenceObject $removeCustomDomains | Where-Object -FilterScript { $_.SideIndicator -eq "<=" } | Select-Object -ExpandProperty InputObject
        $customDomainsToSet = @()
        $customDomainsToSet += $customDomainNames.DefaultDomain # Add default domain
        $customDomainsToSet += $newCustomDomains # Add custom domains to keep

        # Remove domains from Function App
        Write-Verbose "Removing $($removeCustomDomains.count) domains from Function App $FunctionAppName : $($removeCustomDomains -join ", ")..."
        $setAzWebApp = @{
            ResourceGroupName = $ResourceGroupName
            Name              = $FunctionAppName
            HostNames         = $customDomainsToSet
        }
        if ($PSCmdlet.ShouldProcess("Function App $FunctionAppName", "Remove custom domains")) {
            $null = Set-AzWebApp @setAzWebApp -WarningAction SilentlyContinue
        }

        #Remove Managed Certificate if needed
        Write-Verbose "Remove Certificates"
        [Array]$webAppCertificates = Get-AzWebAppCertificate -ResourceGroupName $ResourceGroupName
        foreach ($certificate in $webAppCertificates) {
            $subjectName = $certificate.SubjectName
            if ($subjectName -in $removeCustomDomains) {
                #Get Thumbprint of Certificate
                $thumbprint = $certificate.Thumbprint
                
                #Remove Managed Certificate
                Write-Verbose "Remove Managed Certificate: $subjectName Thumbprint: $thumbprint"
                if ($PSCmdlet.ShouldProcess("Subject: $subjectName, Thumbprint: $thumbprint", "Remove managed certificate")) {
                    # If an error occurs during certificate removal, continue with the next certificate removal as the certificate might be used elsewhere
                    # If we do not continue anyways, users would have to remove the certificate manually for next domains
                    $null = Remove-AzWebAppCertificate -ResourceGroupName $ResourceGroupName -ThumbPrint $thumbprint -Confirm:$false -ErrorAction Continue
                }
            }
        }
    }
}

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 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 DnsServer
        Provide IP address of DNS server to use for DNS queries. Default is 8.8.8.8 (Google DNS).
 
        .PARAMETER DisplayResult
        Provide a Boolean value, if the result should be displayed in the console. Default is $true.
 
        .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 CsvEncoding
        Provide encoding of csv file. Default is "UTF8".
 
        .PARAMETER CsvDelimiter
        Provide delimiter of csv file. Default is ";".
 
        .PARAMETER ExoHostName
        Provide a list of hostnames, which should be included in the MTA-STS policy.
        The list of hostnames must be valid, which means that they must be valid domain names.
        For example, 'contoso.com' or 'fabrikam.com'.
        You can also start with a wildcard character to include subdomains.
        So if you want to include all subdomains of 'contoso.com', you can add '*.contoso.com'.
 
        .EXAMPLE
        Test-PSMTASTSConfiguration -CsvPath "C:\temp\accepted-domains.csv" -FunctionAppName "func-MTA-STS"
 
        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 "func-MTA-STS".
 
        .EXAMPLE
        Test-PSMTASTSConfiguration -DomainName "contoso.com", "fabrikam.com" -FunctionAppName "func-MTA-STS" -ExoHostName "mail.contoso.com"
 
        Checks if MTA-STS is configured correctly for domains "contoso.com" and "fabrikam.com" in Function App "func-MTA-STS". The ExoHostName parameter is used to add the MX record for "mail.contoso.com" to the MTA-STS policy file tests.
 
        .EXAMPLE
        Test-PSMTASTSConfiguration -CsvPath "C:\temp\accepted-domains.csv" -FunctionAppName "func-MTA-STS" -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 "func-MTA-STS". It also exports result to "C:\temp\mta-sts-result.csv".
    #>

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

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

        [String]
        $DnsServer = "8.8.8.8",

        [Bool]
        $DisplayResult = $true,

        [Switch]
        $ExportResult,
    
        [String]
        $ResultPath = (Join-Path -Path $env:TEMP -ChildPath "$(Get-Date -Format yyyyMMddhhmmss)_mta-sts-test-result.csv"),

        [string]
        $CsvEncoding = "UTF8",

        [string]
        $CsvDelimiter = ";",

        [ValidateScript({
                # the provided list of hostnames must be valid, which means that they must be valid domain names
                # Domain Names cannot start with a dot, so we need to check for that
                # Additionally, they can start with a wildcard, if subdomains should be included
                foreach ($hostname in $_) {
                    if ($hostname -notmatch "^[a-zA-Z0-9][a-zA-Z0-9-_.]*$" -and $hostname -notmatch "^\*.[a-zA-Z0-9][a-zA-Z0-9-_.]*$") {
                        throw "MX endpoint '$hostname' is not valid. The name can contain only letters, numbers, hyphens, underscores and periods. You can also start with a wildcard character to include subdomains."
                    }

                    if ($hostname -like "*mail.protection.outlook.com") {
                        throw "MX endpoint '$hostname' cannot be added, because the DNS zone '*.mail.protection.outlook.com' will be added automatically. Please remove it from the list."
                    }
                }
                # if the list of entries is valid, return $true
                $true
            })]
        [String[]]
        $ExoHostName
    )

    begin {
        # Trap errors
        trap {
            throw $_
        }

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Prepare result array
        $result = @()

        # Import csv file with accepted domains
        if ($CsvPath) {
            Write-Verbose "Importing csv file from $CsvPath"
            $domainList = Import-Csv -Path $CsvPath -Encoding $CsvEncoding -Delimiter $CsvDelimiter | Select-Object -ExpandProperty DomainName
        }
    }

    process {
        # Import domains from input parameter
        if ($DomainName) {
            Write-Verbose "Creating array of domains from input parameter"
            $domainList = @()
            foreach ($domain in $DomainName) { $domainList += $domain }
        }

        # Check, if domains have correct format
        foreach ($domain in $domainList) {
            if ($domain -notmatch "^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$") {
                throw -Message "Domain $domain has incorrect format. Please provide domain in format 'contoso.com'."
            }
        }

        # Prepare variables
        ## MTA-STS TXT Record
        $txtRecordContent = "v=STSv1; id=*Z;"
        ## MTA-STS Policy File
        $mtaStsPolicyFileContent = @"
version: STSv1
mode: *
~mxRecords~
max_age: 604800
"@

        [string]$mxHostNames = "mx: *.mail.protection.outlook.com`n" # Add default MX record, which is always added
        foreach ($hostName in $ExoHostName) {
            $mxHostNames += "mx: $hostName`n" # Add MX record for each specified hostname
        }
        $mtaStsPolicyFileContent = $mtaStsPolicyFileContent.replace("~mxRecords~", ($mxHostNames.TrimEnd("`n")))
        ## Counter for verbose output
        $counter = 1

        # Loop through all domains
        $domainListCount = $domainList.Count
        foreach ($domain in $domainList) {
            Write-Verbose "Checking $counter / $($domainList.count) - $($domain)"

            $CNameHost = Resolve-DnsName -Name "mta-sts.$Domain" -Type "CNAME"
            If ($Null -ne $CNameHost) {
                $Hostname = $CNameHost.NameHost
            }
            else {
                $Hostname = ""
            }

            #Progress Bar
            [int]$Percent = 100 / $domainListCount * $counter
            Write-Progress -Activity "Test in Progress" -Status "$Percent% Complete:" -PercentComplete $Percent

            # Prepare result object
            $resultObject = [PSCustomObject]@{
                DomainName            = $domain
                Host                  = $Hostname
                MX_Record_Pointing_To = ""
                # MX_Record_Result = ""
                MTA_STS_CNAME         = ""
                MTA_STS_Policy        = ""
                MTA_STS_PolicyContent = ""
                MTA_STS_TXTRecord     = ""
                MTA_STS_OVERALL       = "OK"
                TLSRPT                = ""
            }

            # Checking MTA-STS TXT Record
            # Example: _mta-sts.example.com. IN TXT "v=STSv1; id=20160831085700Z"
            $mtaStsDNSHost = "_mta-sts." + $domain
            $mtaStsTXTRecord = Resolve-PSMTASTSDnsName -Name $mtaStsDNSHost -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue | Where-Object { $_.Strings -match "v=STSv1" }

            if ($mtaStsTXTRecord -and ($mtaStsTXTRecord.strings -like $txtRecordContent)) {
                $resultObject.MTA_STS_TXTRecord = "OK"
            }
            elseif ($mtaStsTXTRecord -and ($mtaStsTXTRecord.strings -notlike $txtRecordContent)) {
                $resultObject.MTA_STS_TXTRecord = "TXT record does not contain the expected content. The following content was found: $($mtaStsTXTRecord.strings -join ", ")"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
            else {
                $resultObject.MTA_STS_TXTRecord = "TXT record was not found."
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            # Check MTA-STS CNAME record
            $mtaStsName = "mta-sts." + $domain
            $cnameRecord = Resolve-PSMTASTSDnsName -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"
            }
    
            # Checking MTA-STS Policy
            $mtaStsUri = "https://mta-sts.{0}/.well-known/mta-sts.txt" -f $domain
            Write-Verbose "...checking MTA-STS Policy file at $mtaStsUri"
            
            try {
                $mtaStsPolicyResponse = Invoke-WebRequest -Uri $mtaStsUri -TimeoutSec 20 -ErrorAction Stop
                $resultObject.MTA_STS_PolicyContent = ($mtaStsPolicyResponse.Content).Trim()
            }
            catch {
                $resultObject.MTA_STS_Policy = "ERROR"
                $resultObject.MTA_STS_PolicyContent = $_.Exception.Message
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            if (($resultObject.MTA_STS_PolicyContent -like $mtaStsPolicyFileContent) -and (($resultObject.MTA_STS_PolicyContent -like "*mode: enforce*") -or ($resultObject.MTA_STS_PolicyContent -like "*mode: testing*") -or ($resultObject.MTA_STS_PolicyContent -like "*mode: none*"))) {
                $resultObject.MTA_STS_Policy = "OK"
            }
            else {
                $resultObject.MTA_STS_Policy = "Policy file does not contain the expected content.`nCurrently allowed mx records are: $($mxHostNames -join ", ").`nHave you forgotten to add the MX records to the policy file using the '-ExoHostName' parameter?"
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }

            # Checking TLSRPT Record
            # Example: _smtp._tls.example.com. IN TXT "v=TLSRPTv1;rua=mailto:reports@example.com"
            $tlsRptDNSHost = "_smtp._tls." + $domain
            $tlsRptRecord = Resolve-PSMTASTSDnsName -Name $tlsRptDNSHost -Type TXT -Server $DnsServer -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Strings -like "v=TLSRPTv1*" }

            if ($null -ne $tlsRptRecord) {
                $resultObject.TLSRPT = $tlsRptRecord.Strings[0]
            }

            # Checking MX Record
            # Example: example.com. IN MX 0 example-com.mail.protection.outlook.com.
            $mxRecord = Resolve-PSMTASTSDnsName -Name $domain -Type MX -Server $DnsServer -ErrorAction SilentlyContinue

            $resultObject.MX_Record_Pointing_To = $mxRecord.NameExchange -join ", "

            <# Removed the MX record check, because we cannot tell foreach domain, which MX record is correct. Previously, this was possible, as we only checked for the single Exchange Online MX record *.mail.protection.outlook.com.
            # Check if the domain is an onmicrosoft.com domain
            if ($domain -like "*.onmicrosoft.com") {
                $resultObject.MX_Record_Result = "WARNING: You cannot configure MTA-STS for an onmicrosoft.com domain."
                $resultObject.MTA_STS_OVERALL = "ISSUE_FOUND"
            }
            # Check if the MX record points to Exchange Online
            elseif (($mxRecord.NameExchange.count -eq 1) -and ($mxRecord.NameExchange -like $ExoHost)) {
                # MX record points to Exchange Online (only)
                $resultObject.MX_Record_Result = "OK"
            }
            # Check if the MX record points to another host than Exchange Online or if multiple MX records were found
            elseif (($mxRecord.NameExchange.count -gt 1) -or ($mxRecord.NameExchange -notlike $ExoHost)) {
                $resultObject.MX_Record_Result = "WARNING: MX Record does not point to Exchange Online (only). The following host(s) was/were found: $($mxRecord.NameExchange -join ", ")"
            }
            # Assume, that no MX record was found
            else {
                $resultObject.MX_Record_Result = "ERROR: No MX record found. Please assure, that the MX record for $domain points to Exchange Online."
            }
            #>


            # Add the result to the result array
            Write-Verbose "...adding result to result array."
            $result += $resultObject

            # Increase the counter for verbose output
            $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) {
            Write-Warning "Exporting result to $ResultPath"
            $result | Export-Csv -Path $ResultPath -Encoding UTF8 -Delimiter ";" -NoTypeInformation
        }
    }
}

function Update-PSMTASTSFunctionAppFile {
    <#
    .SYNOPSIS
        Publishes Azure Function App files and functions to Azure Function App.
 
    .DESCRIPTION
        Publishes Azure Function App files and functions to Azure Function App.
        The Azure Function App will be updated with the latest PowerShell code.
        This will overwrite any changes you may have made to the Azure Function App!
         
    .PARAMETER ResourceGroupName
        Provide the name of the Azure resource group, where the Azure Function App should be updated.
     
    .PARAMETER FunctionAppName
        Provide the name of the Azure Function App, which should be updated.
 
    .PARAMETER PolicyMode
        Specifies if the policy is in "Enforce" mode or "Testing" mode
 
    .PARAMETER ExoHostName
        Provide a list of hostnames, which should be included in the MTA-STS policy.
        The list of hostnames must be valid, which means that they must be valid domain names.
        For example, 'contoso.com' or 'fabrikam.com'.
        You can also start with a wildcard character to include subdomains.
        So if you want to include all subdomains of 'contoso.com', you can add '*.contoso.com'.
        *.mail.protection.outlook.com will be added automatically to the list of hostnames.
 
    .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
        Update-PSMTASTSFunctionAppFile -ResourceGroupName 'rg-PSMTASTS' -FunctionAppName 'func-PSMTASTS' -PolicyMode 'Testing'
         
        Updates the Azure Function App with the name 'PSMTASTS' in the resource group 'PSMTASTS' with policy mode 'Testing'.
        This will overwrite any changes you made to the Azure Function App!
 
    .EXAMPLE
        Update-PSMTASTSFunctionAppFile -ResourceGroupName 'rg-PSMTASTS' -FunctionAppName 'func-PSMTASTS' -PolicyMode 'Enforce' -ExoHostName '*.abcd-v1.mx.microsoft', 'mail.fabrikam.com'
         
        Updates the Azure Function App with the name 'PSMTASTS' in the resource group 'PSMTASTS' with policy mode 'Enforce'.
        Additionally, it adds the MX record '*.abcd-v1.mx.microsoft' and 'mail.fabrikam.com' to the MTA-STS policy.
        The MTA-STS policy file will look like this:
            version: STSv1
            mode: enforce
            mx: *.mail.protection.outlook.com
            mx: *.abcd-v1.mx.microsoft
            mx: mail.fabrikam.com
            max_age: 604800
 
        This will overwrite any changes you made to the Azure Function App!
 
    .LINK
        https://github.com/jklotzsche-msft/PS.MTA-STS
    #>


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

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

        [ValidateSet("Enforce", "Testing", "None")] # ValidateSet is used to limit the possible values
        [String]
        $PolicyMode = "Enforce",

        [ValidateScript({
                # the provided list of hostnames must be valid, which means that they must be valid domain names
                # Domain Names cannot start with a dot, so we need to check for that
                # Additionally, they can start with a wildcard, if subdomains should be included
                foreach ($hostname in $_) {
                    if ($hostname -notmatch "^[a-zA-Z0-9][a-zA-Z0-9-_.]*$" -and $hostname -notmatch "^\*.[a-zA-Z0-9][a-zA-Z0-9-_.]*$") {
                        throw "MX endpoint '$hostname' is not valid. The name can contain only letters, numbers, hyphens, underscores and periods. You can also start with a wildcard character to include subdomains."
                    }

                    if ($hostname -like "*mail.protection.outlook.com") {
                        throw "MX endpoint '$hostname' cannot be added, because the DNS zone '*.mail.protection.outlook.com' will be added automatically. Please remove it from the list."
                    }
                }
                # if the list of entries is valid, return $true
                $true
            })]
        [String[]]
        $ExoHostName
    )
    #endregion Parameter

    begin {
        # Set working directory for temporary files
        $workingDirectory = Join-Path -Path $env:TEMP -ChildPath "PS.MTA-STS_deployment"

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

        # Preset ActionPreference to Stop, if not set by user through common parameters
        if (-not $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ErrorAction')) { $local:ErrorActionPreference = "Stop" }

        # Check if PowerShell is connected to Azure
        if ( -not (Get-AzContext)) {
            Write-Warning "Connecting to Azure service..."
            $null = Connect-AzAccount
        }
    }
    
    process {
        # Create, if resource group doesn't exist already. If it doesn't exist, create it.
        Write-Verbose "Checking if ResourceGroup '$ResourceGroupName' already exists"
        if ($null -eq (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) {
            throw "ResourceGroup '$ResourceGroupName' does not exist. Please create it first."
        }

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

        # Check, if FunctionApp exists already
        Write-Verbose "Checking if Function Account exists in DNS"
        $functionApp = Get-AzFunctionApp -ResourceGroupName $ResourceGroupName -Name $FunctionAppName -WarningAction SilentlyContinue
        if ($null -eq $functionApp) {
            throw "Function App '$FunctionAppName' does not exist. Please create it first using 'New-PSMTASTSFunctionAppDeployment'."
        }

        # Create Function App contents in temporary folder and zip it
        Write-Verbose "Creating Function App content in temp folder..."
        if (Test-Path -Path $workingDirectory) {
            $null = Get-ChildItem -Path $workingDirectory -Recurse | Remove-Item -Recurse -Force -Confirm:$false
        }

        # Create Function App contents
        
        ## App files
        $null = New-Item -Path $workingDirectory -ItemType Directory -ErrorAction SilentlyContinue
        $null = New-Item -Path "$workingDirectory/function" -ItemType Directory
        $null = $script:PSMTASTS_hostJson | Set-Content -Path "$workingDirectory/function/host.json" -Force
        $null = $script:PSMTASTS_profilePs1 | Set-Content -Path "$workingDirectory/function/profile.ps1" -Force
        $null = $script:PSMTASTS_requirementsPsd1 | Set-Content -Path "$workingDirectory/function/requirements.psd1" -Force
        
        ## Root Website function
        $null = New-Item -Path "$workingDirectory/function/WebsiteRoot" -ItemType Directory
        $null = $script:Root_functionJson | Set-Content -Path "$workingDirectory/function/WebsiteRoot/function.json" -Force
        $null = $script:Root_runPs1 | Set-Content -Path "$workingDirectory/function/WebsiteRoot/run.ps1" -Force
        
        ## MTA-STS Policy function
        $null = New-Item -Path "$workingDirectory/function/MTASTS" -ItemType Directory
        $null = $script:PSMTASTS_functionJson | Set-Content -Path "$workingDirectory/function/MTASTS/function.json" -Force
        ### Adding PSMTASTS_runPs1 variable to local scope, so we can adjust it without changing the script variable
        $local:PSMTASTS_runPs1 = $script:PSMTASTS_runPs1
        if ($PolicyMode -ne "enforce") {
            $local:PSMTASTS_runPs1 = $local:PSMTASTS_runPs1.replace("mode: enforce", "mode: $($PolicyMode.ToLower())")
        }
        ### Add ExoHostName to run.ps1. We will add all specified hostnames to the MTA-STS policy.
        [string]$mxHostNames = "mx: *.mail.protection.outlook.com`n" # Add default MX record, which is always added
        foreach ($hostName in $ExoHostName) {
            $mxHostNames += "mx: $hostName`n" # Add MX record for each specified hostname
        }
        $local:PSMTASTS_runPs1 = $local:PSMTASTS_runPs1.replace("~mxRecords~", ($mxHostNames.TrimEnd("`n"))) # Replace placeholder with MX records
        $null = $local:PSMTASTS_runPs1 | Set-Content -Path "$workingDirectory/function/MTASTS/run.ps1" -Force
        
        ## Create Zip
        if ($PSCmdlet.ShouldProcess("Zip Function App", "Create")) {
            $null = Compress-Archive -Path "$workingDirectory/function/*" -DestinationPath "$workingDirectory/Function.zip" -Force
        }

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

$script:PSMTASTS_hostJson = @'
{
    "version": "2.0",
    "extensions": {
        "http": {
        "routePrefix": "",
            "customHeaders": {
                "Permissions-Policy": "geolocation=()",
                "X-Frame-Options": "SAMEORIGIN",
                "Content-Security-Policy": "default-src 'self'",
                "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
                "X-Content-Type-Options": "nosniff",
                "Referrer-Policy": "no-referrer"
            }
        }
    },
    "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.
#
@{
 
}
'@


#Root Function Json
$script:Root_functionJson = @'
{
    "bindings": [
        {
            "route": "/",
            "authLevel": "ANONYMOUS",
            "methods": [
                "get"
            ],
            "type": "httpTrigger",
            "direction": "in",
            "name": "Request"
        },
        {
            "type": "http",
            "direction": "out",
            "name": "Response"
        }
    ]
}
'@


#Root Script
$script:Root_runPs1 = @'
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
 
# Write to the Azure Functions log stream.
Write-Host "Trigger: Static website index has been requested."
 
# Create HTML
$staticWebsiteIndex = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MTA-STS</title>
</head>
<body>
    <hl>MTA-STS Static Website Index</hl>
    <p>Looking for the <a href="./.well-known/mta-sts.txt">mta-sts.txt</a>?</p>
</body>
</html>
"@
 
# Return the response
try {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [System.Net.HttpStatusCode]::OK
        Headers = @{
            "content-type" = "text/html"
        }
        Body = $staticWebsiteIndex
    })
}
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
    })
}
'@


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


#MTA-STS Poliy Script
$script:PSMTASTS_runPs1 = @'
param ($Request, $TriggerMetadata)
 
# Write to the Azure Functions log stream.
Write-Host "Trigger: MTA-STS policy has been requested."
 
# Prepare the response body
## Replace 'enforce' with 'testing' to test the policy without enforcing it
## Or use the "Update-PSMTASTSFunctionAppDeployment" function to update the policy and other function app files to the current version
## if you prefer to use the legacy, hardcoded MTA-STS policy, use the following variable:
# $mtaStsPolicy = @"
# version: STSv1
# mode: enforce
# mx: *.mail.protection.outlook.com
# max_age: 604800
# "@
 
## The following policy has been auto-generated by the PS.MTA-STS module, depending on which MX records have been specific in the "New-PSMTASTSFunctionAppDeployment" or "Update-PSMTASTSFunctionAppFile" function
$mtaStsPolicy = @"
version: STSv1
mode: enforce
~mxRecords~
max_age: 604800
"@
 
# Return the response
try {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [System.Net.HttpStatusCode]::OK
        Headers = @{
            "Content-type" = "text/plain"
        }
        Body = $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
    })
}
'@