tak.exchange.psm1

#region Helpers

function New-ExchangeAutodiscoverReport {
    [CmdletBinding()]
    param (
        [psobject]
        $InputObject,
        [System.IO.FileInfo]
        $FileName = (Join-Path -Path $($env:temp) -ChildPath "ExchangeAutodiscoverReport.html")
    )
    
    begin {
        Remove-Item -Path $FileName -ErrorAction SilentlyContinue
    }
    
    process {
        foreach($key in (Get-Member -InputObject $InputObject -MemberType NoteProperty).Name) {
            $html += $InputObject.$key.Protocol | ConvertTo-Html -As List -Fragment
        }
        $html | Set-Content -Path $FileName
        Write-Host "report at $FileName"
    }
    
    end {
    }
}

function Get-XmlBody([string]$EmailAddress){
[xml]@"
    <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
    <Request>
        <EMailAddress>$EmailAddress</EMailAddress>
        <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
    </Request>
    </Autodiscover>
"@

}

function Get-AutodiscoverURI {
    param (
        [string]$EmailAddress,
        [string]$ComputerName,
        [switch]$ExcludeExplicitO365Endpoint
    )
    $domainName = $EmailAddress -split "@" | Select-Object -Last 1
    $out = @{
        "root" = "https://$domainName/autodiscover/autodiscover.xml";
        "autodiscover" = "https://autodiscover.$domainName/autodiscover/autodiscover.xml";
    }
    
    if($adSrv = Get-AutodiscoverSRV -domainName $domainName) {
        $out.Add("srv",$adSrv)
    }
    
    if($ComputerName) {
        $out.Add("uri","https://$ComputerName/autodiscover/autodiscover.xml")
    }
    
    if (-not($ExcludeExplicitO365Endpoint)) {
        $out.Add("o365","https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml")
    }

    $out
}

function Get-AutodiscoverResponse {
    [CmdletBinding()]
    param(
        [string]$uri,
        [xml]$body,
        [int]$Timeout = 2,
        [pscredential]$Credential
    )
    $params = @{
        "uri" = $uri 
        "Credential" = $Credential
        "Method" = "post" 
        "Body" = $body 
        "Headers" = @{"content-type"="text/xml"} 
        "DisableKeepAlive" = $true
        "TimeoutSec" = $Timeout
    }
    try {
        Write-Verbose "Trying to connect to $uri"
        $adPayload = Invoke-RestMethod @params
        $adPayload.Autodiscover.Response.Account         
    } catch {
        Write-Verbose "Could not connect to $uri"
    }
}   

function Get-AutodiscoverSRV([string]$domainName) {
    $dns = Resolve-DnsName -Name "_autodiscover._tcp.$domainName" -Type SRV -ErrorAction SilentlyContinue | Where-Object {$_.Type -eq "SRV" -and $_.Port -eq 443}
    if($dns) {
        "https://$($dns.NameTarget)/autodiscover/autodiscover.xml"
    }
}
#endregion Helpers

#region Exchange

function Test-ExchangeAutodiscover {
    <#
    .SYNOPSIS
        Test Exchange Autodiscover Web Service.
    .DESCRIPTION
        This function tests the Exchange Autodiscover Web Serivce for a given Emailaddress. If ComputerName is not specified,
        the function tries to look up the Autodiscover service using the Outlook Clients logic. Locally cached and SCP data
        are not evaluated.
    .EXAMPLE
        PS C:\> Test-ExchangeAutodiscover thomas@ntsystems.it -Credential (Get-Credential)
         
        This example tests the Autodiscover service for the given mailbox. It will query dns for autodiscover.ntsystems.it and _autodiscover._tcp.ntsystems.it.
        It will then try to retrieve an Autodiscover payload from https://ntsystems.it, https://autodiscover.ntsystems.it and the Office 365 endpoint.
    .OUTPUTS
        [psobject]
    #>
    
    [CmdletBinding(HelpUri = 'https://ntsystems.it/PowerShell/TAK/test-exchangeautodiscover/')]
    param (
        [string]
        $EmailAddress,
        [string]
        $ComputerName,
        [pscredential]
        $Credential,
        [switch]
        $ExcludeExplicitO365Endpoint,
        [System.IO.FileInfo]
        $Report
    )
    
    begin {
        # Get URI and XML Body for reuqest
        $adURIs = Get-AutodiscoverURI @PSBoundParameters
        $body = Get-XMLBody @PSBoundParameters
    }
    
    process {
        # Create an empty dictionary to store
        $out = @{}
        foreach($key in $adURIs.keys){
            Write-Verbose "Testing $key domain for $EmailAddress : $($adURIs[$key])"
            $r = Get-AutodiscoverResponse -uri $adURIs[$key] -Credential $Credential -body $body
            if($r) {
                $out.add($key,$r)
            }
        }
        # create a new object and write it to the pipeline
        $global:ExchangeAutodiscoverResults = New-Object -TypeName psobject -Property $out | Add-Member -TypeName 'System.TAK.ExchangeAutoDiscover' -PassThru
        Write-Output $global:ExchangeAutodiscoverResults
        
        Write-Host "Results are available through the global variable `$ExchangeAutodiscoverResults for your convenience.`n"

        if($Report) {
            New-ExchangeAutodiscoverReport -InputObject $global:ExchangeAutodiscoverResults -FileName $Report
        }

    }
    
    end {
    }
}

function Get-MxRecord {
    <#
    .SYNOPSIS
        Get MX Records for a domain.
    .DESCRIPTION
        Uses Resolve-DnsName to get MX Records, Priority and the IP Address of the records.
    .EXAMPLE
        PS C:\> Get-MxRecord ntsystems.it
         
        This example gets the MX record for the domain ntsystems.it.
    .INPUTS
        [string]
    .OUTPUTS
        [Selected.Microsoft.DnsClient.Commands.DnsRecord_MX]
    #>

    [CmdletBinding(HelpUri = 'https://ntsystems.it/PowerShell/TAK/get-mxrecord/')]
    param (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [string]
        $Domain,
        [System.Net.IPAddress]
        $Server
    )
    begin {
        $param = @{
            ErrorAction="SilentlyContinue"
            QuickTimeout=$True
        }
        if($Server) {
            $param.Add("Server",$Server)
        }
    }
    process {
        $mx = Resolve-DnsName -Name $domain -Type MX -ErrorAction SilentlyContinue | Where-Object Type -eq "MX"
        if ($mx) {
            $rec = $mx | Select-Object -Property NameExchange,Preference,@{
                    Name = "IPAddress" 
                    Expression = {
                        Resolve-DnsName -Name $_.NameExchange -Type A_AAAA @param | Select-Object -ExpandProperty IPAddress    
                    }
                }
            $rec | Select-Object -Property *,@{
                Name = "PTR"
                Expression = {
                    $_.IpAddress | ForEach-Object {
                        Resolve-DnsName -Name $_ -Type PTR @param | Select-Object -ExpandProperty NameHost    
                    }
                }
            }
        }
    }
}

#endregion


#region Test ADFS
function Test-FederationService {
    <#
    .Synopsis
       Test the ADFS web service
    .DESCRIPTION
       This function uses Invoke-RestMethod to test if the federation service metadata can be retrieved from a given server.
    .EXAMPLE
       Test-FederationService -ComputerName fs.uclab.eu
       This example gets federation service xml information over the server fs.uclab.eu
    #>

    [CmdletBinding(HelpUri = 'https://ntsystems.it/PowerShell/TAK/Test-FederationService/')]
    param(
        # Specifies the name of the federation server
        [Parameter(Mandatory=$true)]
        [validateLength(3,255)]
        [validatepattern("\w\.\w")]
        [string]
        [Alias("Server")]
        $ComputerName
    )

    $uri = "https://$ComputerName/FederationMetadata/2007-06/FederationMetadata.xml"
    # "adfs/ls/idpinitiatedsignon.htm"
    try {
        $webRequest = Invoke-RestMethod -Uri $uri -ErrorAction Stop
        Write-Verbose $webRequest
    } catch {
        Write-Warning "Could not connect to $uri error $_"
        return
    }
    $out = [ordered]@{
        "entityID" = $webRequest.entitydescriptor.entityID
        "xmlns" = $webRequest.entitydescriptor.xmlns
        "Roles" = @{
            "type" = $webRequest.entitydescriptor.RoleDescriptor.type
            "ServiceDisplayName" = $webRequest.entitydescriptor.RoleDescriptor.ServiceDisplayName
        }
    }
    # Create a custom object and add a custom TypeName for formatting before writing to pipeline
    Write-Output (New-Object -TypeName psobject -Property $out) 
}
#endregion Test ADFS

#region SPF
function New-SPFRecord {
    <#
    .SYNOPSIS
        Create SPF record for a given mail domain.
    .DESCRIPTION
        This function helps with creating SPF records for mail domains.
        The SPF record should look something like this:
 
        v=spf1 mx a ptr ip4:127.1.1.1/24 a:host.example.com include:example.com -all
 
        More information: https://www.ietf.org/rfc/rfc4408.txt
    .EXAMPLE
        PS C:\> Get-AcceptedDomain | New-SPFRecord -mx -IncludeDomain spf.protection.outlook.com -IncludeIP 192.0.2.1,2001:DB8::1 -Action Fail
 
        DomainName : uclab.eu
        Record : "v=spf1 mx ip4:192.0.2.1 ip6:2001:DB8::1 include:spf.protection.outlook.com -all"
 
        The above example creates SPF records for all accepted domains in Exchange (Online).
    .INPUTS
        [string]
        [AcceptedDomain]
 
        This function accepts a string or objects with a DomainName property (such as returned by Get-AcceptedDomain) as input.
    .OUTPUTS
        [psobject]
 
        This function writes a custom object to the pipeline.
    .NOTES
        Author: @torggler
    #>

    [CmdletBinding(HelpUri = 'https://ntsystems.it/PowerShell/TAK/New-SPFRecord/')]
    param (
        [Parameter(ValueFromPipelineByPropertyName=$true,
            ValueFromPipeline=$true)]
        [string]
        $DomainName,
        [switch]
        $mx,
        [switch]
        $a,
        [switch]
        $ptr,
        [ipaddress[]]
        $IncludeIP,
        [string]
        $IncludeDomain,
        [string]
        $IncludeHost,
        [ValidateSet("Fail","SoftFail","Neutral")]
        [string]
        $Action = "Fail"
    )   
    process {
        # if run without parameters, set mx to default
        if(-not($PSBoundParameters.Count)) {
            $PSBoundParameters.Add("mx",$true)
            $PSBoundParameters.Add("Action","Fail")
        }

        Write-Verbose "Creating SPF for domain $DomainName"

        $spfBase = "v=spf1"

        switch ($PSBoundParameters.Keys) {
            "mx" { $spfMX = "mx" }
            "ptr" { $spfPTR = "ptr" }
            "a" { $spfA = "a" }
            "IncludeIP" { 
                $i = 0 # use a little counter to avoid inserting unnecessary spaces
                foreach ($ip in $IncludeIP) {
                    switch ($ip.AddressFamily) {
                        InterNetwork { 
                            Write-Verbose "adding ip4 $ip"
                            if($i -eq 0) {
                                $spfIP = "ip4:$($ip.IPAddressToString)" 
                            } else {
                                $spfIP += " ip4:$($ip.IPAddressToString)" 
                            }
                        }
                        InterNetworkV6 { 
                            Write-Verbose "adding ip6 $ip"
                            if($i -eq 0) {
                                $spfIP = "ip6:$($ip.IPAddressToString)"
                            } else {
                                $spfIP += " ip6:$($ip.IPAddressToString)" 
                            }
                        }
                    }
                $i++
                }
            }
            "IncludeDomain" {
                Write-Verbose "adding include:$IncludeDomain"
                $spfDomain = "include:$IncludeDomain"
            }
            "IncludeHost" {
                Write-Verbose "adding include:$IncludeHost"
                $spfHost = "a:$IncludeHost"
            }
            "Action" {
                switch($Action) {
                    "Fail" { 
                        Write-Verbose "Setting action to Fail"
                        $spfAction = "-all" 
                    }
                    "SoftFail" {
                        Write-Verbose "Setting action to SoftFail"
                        $spfAction = "~all"
                    }
                    "Neutral" { 
                        Write-Verbose "Setting action to Neutral"
                        $spfAction = "?all" 
                    }
                }
            }
        }
        $Record = $spfBase,$spfMX,$spfA,$spfPTR,$spfIP,$spfDomain,$spfHost,$spfAction | Where-Object {$_}
        New-Object -TypeName psobject -Property ([ordered]@{
            DomainName = $DomainName
            Record = $Record -join " "
        })        
    }
}
function Get-SPFRecord {
    <#
    .Synopsis
       Get SPF Record for a domain.
    .DESCRIPTION
       This function uses Resolve-DNSName to get the SPF Record for a given domain. Objects with a DomainName property,
       such as returned by Get-AcceptedDomain, can be piped to this function.
    .EXAMPLE
       Get-AcceptedDomain | Get-SPFRecord
 
       This example gets SPF records for all domains returned by Get-AcceptedDomain.
    #>

    [CmdletBinding(HelpUri = 'https://ntsystems.it/PowerShell/TAK/Get-SPFRecord/')]
    param (
        [Parameter(Mandatory=$true,
            ValueFromPipelineByPropertyName=$true,
            ValueFromPipeline=$true)]
        [string]
        $DomainName,
        [string]
        $Server
    )
    process {
        $params = @{
            Type = "txt"
            Name = $DomainName
            ErrorAction = "Stop"
        }
        if($Server) { $params.Add("Server",$Server) }
        try {
            $dns = Resolve-DnsName @params | Where-Object Strings -Match "spf1"
            $dns |Select-Object @{n="DomainName";e={$_.Name}},@{n="Record";E={$_.Strings}}
        } catch {
            Write-Warning $_
        }
    }
}

#endregion SPF