Mailozaurr.psm1

# Get library name, from the PSM1 file name
$LibraryName = 'Mailozaurr'
$Library = "$LibraryName.dll"
$Class = "$LibraryName.Initialize"

$AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue

# Lets find which libraries we need to load
$Default = $false
$Core = $false
$Standard = $false
foreach ($A in $AssemblyFolders.Name) {
    if ($A -eq 'Default') {
        $Default = $true
    } elseif ($A -eq 'Core') {
        $Core = $true
    } elseif ($A -eq 'Standard') {
        $Standard = $true
    }
}
if ($Standard -and $Core -and $Default) {
    $FrameworkNet = 'Default'
    $Framework = 'Standard'
} elseif ($Standard -and $Core) {
    $Framework = 'Standard'
    $FrameworkNet = 'Standard'
} elseif ($Core -and $Default) {
    $Framework = 'Core'
    $FrameworkNet = 'Default'
} elseif ($Standard -and $Default) {
    $Framework = 'Standard'
    $FrameworkNet = 'Default'
} elseif ($Standard) {
    $Framework = 'Standard'
    $FrameworkNet = 'Standard'
} elseif ($Core) {
    $Framework = 'Core'
    $FrameworkNet = ''
} elseif ($Default) {
    $Framework = ''
    $FrameworkNet = 'Default'
} else {
    Write-Error -Message 'No assemblies found'
}
if ($PSEdition -eq 'Core') {
    $LibFolder = $Framework
} else {
    $LibFolder = $FrameworkNet
}

try {
    $ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core

    if (-not ($Class -as [type])) {
        & $ImportModule ([IO.Path]::Combine($PSScriptRoot, 'Lib', $LibFolder, $Library)) -ErrorAction Stop
    } else {
        $Type = "$Class" -as [Type]
        & $importModule -Force -Assembly ($Type.Assembly)
    }
} catch {
    if ($ErrorActionPreference -eq 'Stop') {
        throw
    } else {
        Write-Warning -Message "Importing module $Library failed. Fix errors before continuing. Error: $($_.Exception.Message)"
        # we will continue, but it's not a good idea to do so
        # return
    }
}
# Dot source all libraries by loading external file
. $PSScriptRoot\Mailozaurr.Libraries.ps1

# Dot source all classes by loading external file
. $PSScriptRoot\Mailozaurr.Classes.ps1

function Join-UriQuery { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together including advanced querying
 
    .DESCRIPTION
    Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url (optional)
 
    .PARAMETER QueryParameter
    Parameters and their values in form of hashtable
 
    .PARAMETER EscapeUriString
    If set, will escape the url string
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-UrlQuery')]
    [CmdletBinding()]
    param (
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri,
        [Parameter()][System.Collections.IDictionary] $QueryParameter,
        [alias('EscapeUrlString')][switch] $EscapeUriString
    )
    Begin {
        Add-Type -AssemblyName System.Web
    }
    Process {

        if ($BaseUri -and $RelativeOrAbsoluteUri) {
            $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri
        } else {
            $Url = $BaseUri
        }

        if ($QueryParameter) {
            $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty)
            foreach ($key in $QueryParameter.Keys) {
                $Collection.Add($key, $QueryParameter.$key)
            }
        }

        $uriRequest = [System.UriBuilder] $Url
        if ($Collection) {
            $uriRequest.Query = $Collection.ToString()
        }
        if (-not $EscapeUriString) {
            $uriRequest.Uri.AbsoluteUri
        } else {
            [System.Uri]::EscapeUriString($uriRequest.Uri.AbsoluteUri)
        }
    }
}
function Remove-EmptyValue { 
    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Join-Uri { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together
 
    .DESCRIPTION
    Provides ability to join two Url paths together
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-Url')]
    [cmdletBinding()]
    param(
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri
    )

    return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/'))
}
function Connect-O365Graph {
    [cmdletBinding()]
    param(
        [string][alias('ClientID')] $ApplicationID,
        [string][alias('ClientSecret')] $ApplicationKey,
        [string] $TenantDomain,
        [ValidateSet('https://manage.office.com', 'https://graph.microsoft.com')] $Resource = 'https://manage.office.com'
    )
    # https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau-1

    #$Scope = @(
    #'https://outlook.office.com/IMAP.AccessAsUser.All',
    # 'https://outlook.office.com/POP.AccessAsUser.All',
    # 'https://outlook.office.com/Mail.Send'
    # 'https://outlook.office.com/User.Read'
    #)

    $Body = @{
        grant_type    = 'client_credentials'
        resource      = $Resource
        client_id     = $ApplicationID
        client_secret = $ApplicationKey
        #scope = [System.Web.HttpUtility]::UrlEncode( $Scope)
    }
    try {
        $Authorization = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$($TenantDomain)/oauth2/token" -Body $body -ErrorAction Stop
    } catch {
        $ErrorMessage = $_.Exception.Message -replace "`n", ' ' -replace "`r", ' '
        Write-Warning -Message "Connect-O365Graph - Error: $ErrorMessage"
    }
    if ($Authorization) {
        @{'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)" }
    } else {
        $null
    }
}
function Connect-O365GraphMSAL {
    [cmdletBinding()]
    param(
        [string][alias('ClientSecret')] $ApplicationKey
    )
    @{'Authorization' = "Bearer $ApplicationKey" }
}
function Convert-CloudflareToGoogleCAA {
    [cmdletbinding()]
    param (
        [string]$hexString
    )

    # Remove the leading hash mark and space, and filter out non-hexadecimal characters
    $cleanedHexString = $hexString.TrimStart("# ") -replace "[^0-9A-Fa-f]", ""

    # Convert the cleaned string into an array of bytes
    $bytes = [byte[]]($cleanedHexString -split '(..)' | Where-Object { $_ } | ForEach-Object { [convert]::ToByte($_, 16) })

    # Skip the initial length byte
    $dataBytes = $bytes[1..($bytes.Length - 1)]

    # Extract the flag byte
    $flag = $dataBytes[0]

    # Extract the tag length and tag
    $tagLength = $dataBytes[1]
    $tag = [System.Text.Encoding]::ASCII.GetString($dataBytes[2..($tagLength + 1)])

    # Extract the value
    $value = [System.Text.Encoding]::ASCII.GetString($dataBytes[($tagLength + 2)..($dataBytes.Length - 1)])

    "$flag $tag $value"
}
function Convert-CloudflareToGoogleDane {
    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $DaneRecord
    )
    # Remove the prefix '\# 35 '
    $cloudflareOutput = $DaneRecord -replace '^\\# 35 ', ''

    # Split the string into an array of strings, each containing two characters
    $splitOutput = $cloudflareOutput -split ' '

    # The first three elements represent the certificate usage, selector, and matching type
    # Convert them to integers to remove leading zeros, then back to strings
    $certificateFields = ($splitOutput[0..2] | ForEach-Object { [int]"0x$_" }) -join ' '

    # The remaining elements represent the certificate association data
    $certificateData = $splitOutput[3..($splitOutput.Length - 1)] -join ''

    # Combine the certificate fields and certificate data into the desired format
    $googleFormatOutput = "$certificateFields $certificateData"

    # Now $googleFormatOutput will have the desired format

    $googleFormatOutput
}
function ConvertFrom-GraphCredential {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][PSCredential] $Credential
    )
    if ($Credential.UserName -eq 'MSAL') {
        [PSCustomObject] @{
            ClientID     = 'MSAL'
            ClientSecret = $Credential.GetNetworkCredential().Password
        }
    } else {
        $Object = $Credential.UserName -split '@'
        if ($Object.Count -eq 2) {
            [PSCustomObject] @{
                ClientID     = $Object[0]
                DirectoryID  = $Object[1]
                ClientSecret = $Credential.GetNetworkCredential().Password
            }
        }
    }
}
function ConvertFrom-OAuth2Credential {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][PSCredential] $Credential
    )
    [PSCustomObject] @{
        UserName = $Credential.UserName
        Token    = $Credential.GetNetworkCredential().Password
    }
}
function ConvertTo-GraphAddress {
    [cmdletBinding()]
    param(
        [Array] $MailboxAddress,
        [object] $From,
        [switch] $LimitedFrom
    )
    foreach ($E in $MailboxAddress) {
        if ($E -is [string]) {
            if ($E) {
                if ($E -notlike "*<*>*") {
                    @{
                        emailAddress = @{
                            address = $E
                        }
                    }
                } else {
                    # supports 'User01 <user01@fabrikam.com>'
                    $Mailbox = [MimeKit.MailboxAddress] $E
                    @{
                        emailAddress = @{
                            address = $Mailbox.Address
                        }
                    }
                }
            }
        } elseif ($E -is [System.Collections.IDictionary]) {
            if ($E.Email) {
                @{
                    emailAddress = @{
                        address = $E.Email
                    }
                }
            }
        } elseif ($E -is [MimeKit.MailboxAddress]) {
            if ($E.Address) {
                @{
                    emailAddress = @{
                        address = $E.Address
                    }
                }
            }
        } else {
            if ($E.Name -and $E.Email) {
                @{
                    emailAddress = @{
                        address = $E.Email
                    }
                }
            } elseif ($E.Email) {
                @{
                    emailAddress = @{
                        address = $E.Email
                    }
                }
            }
        }
    }
    if ($From) {
        if ($From -is [string]) {
            if ($From -notlike "*<*>*") {
                if ($LimitedFrom) {
                    $From
                } else {
                    @{
                        emailAddress = @{
                            address = $From
                        }
                    }
                }
            } else {
                # supports 'User01 <user01@fabrikam.com>'
                $Mailbox = [MimeKit.MailboxAddress] $From
                if ($LimitedFrom) {
                    $Mailbox.Address
                } else {
                    @{
                        emailAddress = @{
                            address = $Mailbox.Address
                        }
                    }
                }
            }
        } elseif ($From -is [System.Collections.IDictionary]) {
            if ($LimitedFrom) {
                $From.Email
            } else {
                @{
                    emailAddress = @{
                        address = $From.Name
                        #name = $From.Name
                    }
                }
            }
        } elseif ($From -is [MimeKit.MailboxAddress]) {
            if ($LimitedFrom) {
                $From.Address
            } else {
                @{
                    emailAddress = @{
                        address = $From.Address
                    }
                }
            }
        } else {
            if ($From.Email) {
                if ($LimitedFrom) {
                    $From.Email
                } else {
                    @{
                        emailAddress = @{
                            address = $From.Email
                        }
                    }
                }
            }
        }
    }
}
function ConvertTo-GraphAttachment {
    [CmdletBinding()]
    param(
        [string[]] $Attachment
    )
    foreach ($A in $Attachment) {
        try {
            $ItemInformation = Get-Item -LiteralPath $A -ErrorAction Stop
        } catch {
            Write-Warning -Message "ConvertTo-GraphAttachment: Attachment '$A' processing error. Error: $($_.Exception.Message)"
        }
        if ($ItemInformation) {
            try {
                $File = [system.io.file]::ReadAllBytes($A)
                $Bytes = [System.Convert]::ToBase64String($File)

                @{
                    '@odata.type'  = '#microsoft.graph.fileAttachment'
                    #'@odata.type' = '#Microsoft.OutlookServices.FileAttachment'
                    'name'         = $ItemInformation.Name
                    #'contentType' = 'text/plain'
                    'contentBytes' = $Bytes
                }
            } catch {
                Write-Warning -Message "ConvertTo-GraphAttachment: Attachment '$A' reading error. Error: $($_.Exception.Message)"
            }
        }
    }
}
function ConvertTo-MailboxAddress {
    [cmdletBinding()]
    param(
        [Array] $MailboxAddress
    )
    foreach ($_ in $MailboxAddress) {
        if ($_ -is [string]) {
            if ($_ -notlike "*<*>*") {
                $SmtpTo = [MimeKit.MailboxAddress]::new("$_", "$_")
            } else {
                $SmtpTo = [MimeKit.MailboxAddress] $_
            }
        } elseif ($_ -is [System.Collections.IDictionary]) {
            $SmtpTo = [MimeKit.MailboxAddress]::new($_.Name, $_.Email)
        } elseif ($_ -is [MimeKit.MailboxAddress]) {
            $SmtpTo = $_
        } else {
            if ($_.Name -and $_.Email) {
                $SmtpTo = [MimeKit.MailboxAddress]::new($_.Name, $_.Email)
            } elseif ($_.Email) {
                $SmtpTo = [MimeKit.MailboxAddress]::new($_.Email, $_.Email)
            }
        }
        $SmtpTo
    }
}
function ConvertTo-SendGridAddress {
    [cmdletBinding()]
    param(
        [Array] $MailboxAddress,
        [alias('ReplyTo')][object] $From,
        [switch] $LimitedFrom
    )
    foreach ($E in $MailboxAddress) {
        if ($E -is [string]) {
            if ($E) {
                if ($E -notlike "*<*>*") {
                    @{ email = $E }
                } else {
                    # supports 'User01 <user01@fabrikam.com>'
                    $Mailbox = [MimeKit.MailboxAddress] $E
                    @{ email = $Mailbox.Address }
                }
            }
        } elseif ($E -is [System.Collections.IDictionary]) {
            if ($E.Email) {
                @{ email = $E.Email }
            }
        } elseif ($E -is [MimeKit.MailboxAddress]) {
            if ($E.Address) {
                @{ email = $E.Address }
            }
        } else {
            if ($E.Name -and $E.Email) {
                @{
                    email = $E.Email
                    name  = $E.Name
                }
            } elseif ($E.Email) {
                @{ email = $E.Email }
            }
        }
    }
    if ($From) {
        if ($From -is [string]) {
            if ($LimitedFrom) {
                $From
            } else {
                @{ email = $From }
            }
        } elseif ($From -is [System.Collections.IDictionary]) {
            if ($LimitedFrom) {
                $From.Email
            } else {
                @{
                    email = $From.Email
                    name  = $From.Name
                }
            }
        } elseif ($From -is [MimeKit.MailboxAddress]) {
            if ($LimitedFrom) {
                $From.Address
            } else {
                @{
                    email = $From.Address
                    name  = $From.Name
                }
            }
        } else {
            if ($From.Email) {
                @{
                    email = $From.Email
                    name  = $From.Name
                }
            } elseif ($From.Email) {
                @{ email = $From.Email }
            }
        }
    }
}
function Invoke-O365Graph {
    [cmdletBinding()]
    param(
        [uri] $PrimaryUri = 'https://graph.microsoft.com/v1.0',
        [uri] $Uri,
        [alias('Authorization')][System.Collections.IDictionary] $Headers,
        [validateset('GET', 'DELETE', 'POST')][string] $Method = 'GET',
        [string] $ContentType = 'application/json',
        [switch] $FullUri,
        [switch] $MgGraphRequest
    )
    $RestSplat = @{
        Headers     = $Headers
        Method      = $Method
        ContentType = $ContentType
    }
    if ($FullUri) {
        $RestSplat.Uri = $Uri
    } else {
        $RestSplat.Uri = -join ($PrimaryUri, $Uri)
    }
    try {
        if ($MgGraphRequest) {
            $OutputQuery = Invoke-MgGraphRequest @RestSplat -Verbose:$false
        } else {
            $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false
        }
        if ($Method -eq 'GET') {
            if ($OutputQuery.value) {
                foreach ($Mail in $OutputQuery.value) {
                    [PSCustomObject] $Mail
                }
            }
            if ($OutputQuery.'@odata.nextLink') {
                $RestSplat.Uri = $OutputQuery.'@odata.nextLink'
                $MoreData = Invoke-O365Graph @RestSplat -FullUri -MgGraphRequest:$MgGraphRequest.IsPresent
                if ($MoreData) {
                    $MoreData
                }
            }
        } else {
            return $true
        }
    } catch {
        $RestError = $_.ErrorDetails.Message
        if ($RestError) {
            try {
                $RestError -split '\r\n' | ForEach-Object {
                    if ($_ -match '^{') {
                        $RestError = $_
                    }
                }
                $ErrorMessage = ConvertFrom-Json -InputObject $RestError
                $ErrorMy = -join ('JSON Error:' , $ErrorMessage.error.code, ' ', $ErrorMessage.error.message, ' Additional Error: ', $_.Exception.Message)
                Write-Warning $ErrorMy
            } catch {
                Write-Warning $_.Exception.Message
            }
        } else {
            Write-Warning $_.Exception.Message
        }
        if ($Method -ne 'GET') {
            return $false
        }
    }
}
function IsLargerThan {
    [CmdletBinding()]
    param(
        [string[]] $FilePath,
        [int] $Size = 4000000
    )
    $AttachmentsSize = 0
    foreach ($A in $FilePath) {
        try {
            $ItemInformation = Get-Item -LiteralPath $A -ErrorAction Stop
            $AttachmentsSize += $ItemInformation.Length
        } catch {
            Write-Warning -Message "ConvertTo-GraphAttachment: Attachment '$A' processing error. Error: $($_.Exception.Message)"
        }
    }
    if ($AttachmentsSize -gt $Size) {
        $true
    }
}
function New-GraphAttachment {
    [cmdletbinding()]
    param(
        [PSCustomObject] $DraftMessage,
        [string] $FromField,
        [System.Collections.IDictionary] $Authorization,
        [string[]] $Attachments,
        [switch] $MgGraphRequest
    )
    [Int32] $UploadChunkSize = 9000000 # 9MB chunks
    $Authorization['AnchorMailbox'] = $FromField
    foreach ($Attachment in $Attachments) {
        $StopWatchAttachment = [System.Diagnostics.Stopwatch]::StartNew()
        Write-Verbose -Message "New-GraphAttachment - Uploading attachment '$Attachment'"
        $UploadSession = "https://graph.microsoft.com/v1.0/users('" + $FromField + "')/messages/" + $DraftMessage.id + "/attachments/createUploadSession"
        try {
            $FileStream = [System.IO.StreamReader]::new($Attachment)
            $FileSize = $FileStream.BaseStream.Length
            $FileName = [System.IO.Path]::GetFileName($Attachment)
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                throw
            } else {
                Write-Warning "New-GraphAttachment - Error reading attachment: $($_.Exception.Message) $ErrorDetails"
            }
            continue
        }
        $File = @{
            "AttachmentItem" = [ordered] @{
                "attachmentType" = "file"
                "name"           = $FileName
                "size"           = $FileSize
            }
        }
        $FileJson = $File | ConvertTo-Json -Depth 2

        try {
            if ($MgGraphRequest) {
                $Results = Invoke-MgGraphRequest -Uri $UploadSession -Method POST -Body $FileJson -ErrorAction Stop -ContentType 'application/json; charset=UTF-8' -UserAgent "Mailozaurr"
            } else {
                $Results = Invoke-RestMethod -Method POST -Uri $UploadSession -Headers $Authorization -Body $FileJson -ContentType 'application/json; charset=UTF-8' -UserAgent "Mailozaurr" -ErrorAction Stop
            }
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                throw
            } else {
                Write-Warning "New-GraphAttachment - Error creating upload session: $($_.Exception.Message) $ErrorDetails"
            }
            continue
        }
        $UploadUrl = $Results.uploadUrl
        if ($UploadChunkSize -gt $FileStream.BaseStream.Length) {
            $UploadChunkSize = $FileStream.BaseStream.Length
        }
        $FileOffsetStart = 0
        $FileBuffer = [byte[]]::new($UploadChunkSize)
        do {
            $FileChunkByteCount = $FileStream.BaseStream.Read($FileBuffer, 0, $FileBuffer.Length)
            $FileOffsetEnd = $FileStream.BaseStream.Position - 1
            if ($FileChunkByteCount -gt 0) {
                $UploadRangeHeader = "bytes " + $FileOffsetStart + "-" + $FileOffsetEnd + "/" + $FileSize
                $FileOffsetStart = $FileStream.BaseStream.Position
                $BinaryContent = [System.Net.Http.ByteArrayContent]::new($FileBuffer, 0, $FileChunkByteCount)
                $FileBuffer = [byte[]]::new($UploadChunkSize)
                $Headers = @{
                    "Content-Range"  = $UploadRangeHeader
                    "AnrchorMailbox" = $FromField
                }
                try {
                    if ($MgGraphRequest) {
                        $Results = Invoke-MgGraphRequest -Uri $UploadUrl -Method PUT -Body $BinaryContent.ReadAsByteArrayAsync().Result -ErrorAction Stop -ContentType 'application/octet-stream' -UserAgent "Mailozaurr" -Verbose:$false
                    } else {
                        $Results = Invoke-RestMethod -Method PUT -Uri $UploadUrl -Headers $Headers -Body $BinaryContent.ReadAsByteArrayAsync().Result -ContentType "application/octet-stream" -UserAgent "Mailozaurr" -Verbose:$false
                    }
                } catch {
                    if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                        throw
                    } else {
                        Write-Warning "New-GraphAttachment - Error sending bytes to upload session: $($_.Exception.Message) $ErrorDetails"
                    }
                    break
                }
            }
        } while ($FileChunkByteCount -ne 0)

        $Authorization.Remove('AnchorMailbox')
        $StopWatchAttachment.Stop()
        Write-Verbose -Message "New-GraphAttachment - Attachment '$Attachment' uploaded in $($StopWatchAttachment.Elapsed.TotalSeconds) seconds"
    }
}
function New-GraphDraftMessage {
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [string] $MailSentTo,
        [string] $FromField,
        [System.Collections.IDictionary] $Authorization,
        [string] $Body,
        [switch] $MgGraphRequest
    )

    Try {
        if ($PSCmdlet.ShouldProcess("$MailSentTo", 'Send-EmailMessage')) {
            $Uri = "https://graph.microsoft.com/v1.0/users/$FromField/mailfolders/drafts/messages"
            if ($MgGraphRequest) {
                $OutputRest = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $Body -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            } else {
                $OutputRest = Invoke-RestMethod -Uri $Uri -Headers $Authorization -Method POST -Body $Body -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            }
            $OutputRest
        }
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            throw
            return
        }
        $RestError = $_.ErrorDetails.Message
        $RestMessage = $_.Exception.Message
        if ($RestError) {
            try {
                $ErrorMessage = ConvertFrom-Json -InputObject $RestError -ErrorAction Stop
                $ErrorText = $ErrorMessage.error.message
                Write-Warning -Message "Send-GraphMailMessage - Error during draft message creation: $($RestMessage) $($ErrorText)"
            } catch {
                $ErrorText = ''
                Write-Warning -Message "Send-GraphMailMessage - Error during draft message creation: $($RestMessage)"
            }
        } else {
            Write-Warning -Message "Send-GraphMailMessage - Error during draft message creation: $($_.Exception.Message)"
        }
        if ($_.ErrorDetails.RecommendedAction) {
            Write-Warning -Message "Send-GraphMailMessage - Error during draft message creation. Recommended action: $RecommendedAction"
        }
    }
}
function New-GraphSendMessage {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [System.Diagnostics.Stopwatch] $StopWatch,
        [string] $MailSentTo,
        [string] $FromField,
        [System.Collections.IDictionary] $Authorization,
        [string] $Body,
        [switch] $Suppress,
        [switch] $MgGraphRequest
    )
    Try {
        if ($PSCmdlet.ShouldProcess("$MailSentTo", 'Send-EmailMessage')) {
            $Uri = "https://graph.microsoft.com/v1.0/users/$FromField/sendMail"
            if ($MgGraphRequest) {
                $null = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $Body -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            } else {
                $null = Invoke-RestMethod -Uri $Uri -Headers $Authorization -Method POST -Body $Body -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            }
            if (-not $Suppress) {
                [PSCustomObject] @{
                    Status        = $True
                    Error         = ''
                    SentTo        = $MailSentTo
                    SentFrom      = $FromField
                    Message       = ''
                    TimeToExecute = $StopWatch.Elapsed
                }
            }
        } else {
            if (-not $Suppress) {
                [PSCustomObject] @{
                    Status        = $false
                    Error         = 'Email not sent (WhatIf)'
                    SentTo        = $MailSentTo
                    SentFrom      = $FromField
                    Message       = ''
                    TimeToExecute = $StopWatch.Elapsed
                }
            }
        }
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        }
        $RestError = $_.ErrorDetails.Message
        $RestMessage = $_.Exception.Message
        if ($RestError) {
            try {
                $ErrorMessage = ConvertFrom-Json -InputObject $RestError -ErrorAction Stop
                $ErrorText = $ErrorMessage.error.message
                Write-Warning -Message "Send-GraphMailMessage - Error: $($RestMessage) $($ErrorText)"
            } catch {
                $ErrorText = ''
                Write-Warning -Message "Send-GraphMailMessage - Error: $($RestMessage)"
            }
        } else {
            Write-Warning -Message "Send-GraphMailMessage - Error: $($_.Exception.Message)"
        }
        if ($_.ErrorDetails.RecommendedAction) {
            Write-Warning -Message "Send-GraphMailMessage - Recommended action: $RecommendedAction"
        }
        if (-not $Suppress) {
            [PSCustomObject] @{
                Status        = $False
                Error         = if ($RestError) {
                    "$($RestMessage) $($ErrorText)" 
                } else {
                    $RestMessage 
                }
                SentTo        = $MailSentTo
                SentFrom      = $FromField
                Message       = ''
                TimeToExecute = $StopWatch.Elapsed
            }
        }
    }
}
[string[]] $Script:BlockList = @(
    'b.barracudacentral.org'
    'spam.rbl.msrbl.net'
    'zen.spamhaus.org'
    'bl.deadbeef.com'
    #'bl.emailbasura.org' dead as per https://github.com/EvotecIT/PSBlackListChecker/issues/8
    'bl.spamcop.net'
    'blackholes.five-ten-sg.com'
    'blacklist.woody.ch'
    'bogons.cymru.com'
    'cbl.abuseat.org'
    'combined.abuse.ch'
    'combined.rbl.msrbl.net'
    'db.wpbl.info'
    'dnsbl-1.uceprotect.net'
    'dnsbl-2.uceprotect.net'
    'dnsbl-3.uceprotect.net'
    'dnsbl.cyberlogic.net'
    'dnsbl.inps.de'
    'dnsbl.sorbs.net'
    'drone.abuse.ch'
    'drone.abuse.ch'
    'duinv.aupads.org'
    'dul.dnsbl.sorbs.net'
    'dul.ru'
    'dyna.spamrats.com'
    # 'dynip.rothen.com' dead as per https://github.com/EvotecIT/PSBlackListChecker/issues/9
    'http.dnsbl.sorbs.net'
    'images.rbl.msrbl.net'
    'ips.backscatterer.org'
    'ix.dnsbl.manitu.net'
    'korea.services.net'
    'misc.dnsbl.sorbs.net'
    'noptr.spamrats.com'
    'ohps.dnsbl.net.au'
    'omrs.dnsbl.net.au'
    'orvedb.aupads.org'
    'osps.dnsbl.net.au'
    'osrs.dnsbl.net.au'
    'owfs.dnsbl.net.au'
    'owps.dnsbl.net.au'
    'pbl.spamhaus.org'
    'phishing.rbl.msrbl.net'
    'probes.dnsbl.net.au'
    'proxy.bl.gweep.ca'
    'proxy.block.transip.nl'
    'psbl.surriel.com'
    'rbl.interserver.net'
    'rdts.dnsbl.net.au'
    'relays.bl.gweep.ca'
    'relays.bl.kundenserver.de'
    'relays.nether.net'
    'residential.block.transip.nl'
    'ricn.dnsbl.net.au'
    'rmst.dnsbl.net.au'
    'sbl.spamhaus.org'
    'short.rbl.jp'
    'smtp.dnsbl.sorbs.net'
    'socks.dnsbl.sorbs.net'
    'spam.abuse.ch'
    'spam.dnsbl.sorbs.net'
    'spam.spamrats.com'
    'spamlist.or.kr'
    'spamrbl.imp.ch'
    't3direct.dnsbl.net.au'
    'ubl.lashback.com'
    'ubl.unsubscore.com'
    'virbl.bit.nl'
    'virus.rbl.jp'
    'virus.rbl.msrbl.net'
    'web.dnsbl.sorbs.net'
    'wormrbl.imp.ch'
    'xbl.spamhaus.org'
    'zombie.dnsbl.sorbs.net'
    #'bl.spamcannibal.org' now a parked domain
    #'tor.ahbl.org' # as per https://ahbl.org/ was terminated in 2015
    #'tor.dnsbl.sectoor.de' parked domain
    #'torserver.tor.dnsbl.sectoor.de' as above
    #'dnsbl.njabl.org' # supposedly doesn't work properly anymore
    # 'dnsbl.ahbl.org' # as per https://ahbl.org/ was terminated in 2015
    # 'cdl.anti-spam.org.cn' Inactive
)
$Script:DNSTypes = @{
    A          = '1'
    NS         = '2'
    MD         = '3'
    MF         = '4'
    CNAME      = '5'
    SOA        = '6'
    MB         = '7'
    MG         = '8'
    MR         = '9'
    NULL       = '10'
    WKS        = '11'
    PTR        = '12'
    HINFO      = '13'
    MINFO      = '14'
    MX         = '15'
    TXT        = '16'
    RP         = '17'
    AFSDB      = '18'
    X25        = '19'
    ISDN       = '20'
    RT         = '21'
    NSAP       = '22'
    NSAPPTR    = '23'
    SIG        = '24'
    KEY        = '25'
    PX         = '26'
    GPOS       = '27'
    AAAA       = '28'
    LOC        = '29'
    NXT        = '30'
    EID        = '31'
    NIMLOC     = '32'
    SRV        = '33'
    ATMA       = '34'
    NAPTR      = '35'
    KX         = '36'
    CERT       = '37'
    A6         = '38'
    DNAME      = '39'
    SINK       = '40'
    OPT        = '41'
    APL        = '42'
    DS         = '43'
    SSHFP      = '44'
    IPSECKEY   = '45'
    RRSIG      = '46'
    NSEC       = '47'
    DNSKEY     = '48'
    DHCID      = '49'
    NSEC3      = '50'
    NSEC3PARAM = '51'
    TLSA       = '52'
    SMIMEA     = '53'
    Unassigned = '54'
    HIP        = '55'
    NINFO      = '56'
    RKEY       = '57'
    TALINK     = '58'
    CDS        = '59'
    CDNSKEY    = '60'
    OPENPGPKEY = '61'
    CSYNC      = '62'
    SPF        = '99'
    UINFO      = '100'
    UID        = '101'
    GID        = '102'
    UNSPEC     = '103'
    NID        = '104'
    L32        = '105'
    L64        = '106'
    LP         = '107'
    EUI48      = '108'
    EUI64      = '109'
    TKEY       = '249'
    TSIG       = '250'
    IXFR       = '251'
    AXFR       = '252'
    MAILB      = '253'
    MAILA      = '254'
    All        = '255'
    URI        = '256'
    CAA        = '257'
    AVC        = '258'
    DOA        = '259'
    TA         = '32768'
    DLV        = '32769'
}

$Script:DNSQueryTypes = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    $Script:DNSTypes.Keys | Where-Object { $_ -like "*$wordToComplete*" }
}
function Send-GraphMailMessage {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [object] $From,
        [Array] $To,
        [Array] $Cc,
        [Array] $Bcc,
        [string] $ReplyTo,
        [string] $Subject,
        [alias('Body')][string[]] $HTML,
        [string[]] $Text,
        [alias('Attachments')][string[]] $Attachment,
        [PSCredential] $Credential,
        [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority,
        [switch] $DoNotSaveToSentItems,
        [switch] $RequestReadReceipt,
        [switch] $RequestDeliveryReceipt,
        [System.Diagnostics.Stopwatch] $StopWatch,
        [switch] $Suppress,
        [switch] $MgGraphRequest
    )
    if ($MgGraphRequest) {
        # it's already connected
    } elseif ($Credential) {
        $AuthorizationData = ConvertFrom-GraphCredential -Credential $Credential
    } else {
        return
    }
    if ($AuthorizationData) {
        if ($AuthorizationData.ClientID -eq 'MSAL') {
            $Authorization = Connect-O365GraphMSAL -ApplicationKey $AuthorizationData.ClientSecret
        } else {
            $Authorization = Connect-O365Graph -ApplicationID $AuthorizationData.ClientID -ApplicationKey $AuthorizationData.ClientSecret -TenantDomain $AuthorizationData.DirectoryID -Resource https://graph.microsoft.com
        }
    }
    $Body = @{}
    if ($HTML) {
        $Body['contentType'] = 'HTML'
        $body['content'] = $HTML -join [System.Environment]::NewLine
    } elseif ($Text) {
        $Body['contentType'] = 'Text'
        $body['content'] = $Text -join [System.Environment]::NewLine
    } else {
        $Body['contentType'] = 'Text'
        $body['content'] = ''
    }

    $Message = [ordered] @{
        # https://docs.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0
        message         = [ordered] @{
            subject                    = $Subject
            body                       = $Body
            from                       = ConvertTo-GraphAddress -From $From
            toRecipients               = @(
                ConvertTo-GraphAddress -MailboxAddress $To
            )
            ccRecipients               = @(
                ConvertTo-GraphAddress -MailboxAddress $CC
            )
            bccRecipients              = @(
                ConvertTo-GraphAddress -MailboxAddress $BCC
            )
            #sender = @(
            # ConvertTo-GraphAddress -MailboxAddress $From
            #)
            replyTo                    = @(
                ConvertTo-GraphAddress -MailboxAddress $ReplyTo
            )
            importance                 = $Priority
            isReadReceiptRequested     = $RequestReadReceipt.IsPresent
            isDeliveryReceiptRequested = $RequestDeliveryReceipt.IsPresent
        }
        saveToSentItems = -not $DoNotSaveToSentItems.IsPresent
    }
    $MailSentTo = -join ($To -join ',', $CC -join ', ', $Bcc -join ', ')
    $FromField = ConvertTo-GraphAddress -From $From -LimitedFrom
    Remove-EmptyValue -Hashtable $Message -Recursive -Rerun 2

    if ($Attachment -and (IsLargerThan -FilePath $Attachment -Size 3000000)) {
        $BodyDraft = $Message.Message | ConvertTo-Json -Depth 5
        $DraftMessage = New-GraphDraftMessage -Body $BodyDraft -MailSentTo $MailSentTo -Authorization $Authorization -FromField $FromField -MgGraphRequest:$MgGraphRequest.IsPresent
        $null = New-GraphAttachment -DraftMessage $DraftMessage -FromField $FromField -Attachments $Attachment -Authorization $Authorization -MgGraphRequest:$MgGraphRequest.IsPresent
        Send-GraphMailMessageDraft -DraftMessage $DraftMessage -Authorization $Authorization -FromField $FromField -StopWatch $StopWatch -Suppress:$Suppress -MailSentTo $MailSentTo -MgGraphRequest:$MgGraphRequest.IsPresent
    } else {
        # No attachments or attachments are under 4MB
        if ($Attachment) {
            $Message['message']['attachments'] = @(ConvertTo-GraphAttachment -Attachment $Attachment)
        }
        $Body = $Message | ConvertTo-Json -Depth 5
        New-GraphSendMessage -Body $Body -StopWatch $StopWatch -MailSentTo $MailSentTo -Authorization $Authorization -FromField $FromField -Suppress:$Suppress -MgGraphRequest:$MgGraphRequest.IsPresent
    }
    if ($VerbosePreference) {
        if ($Message.message.attachments) {
            $Message.message.attachments | ForEach-Object {
                if ($_.contentBytes.Length -ge 10) {
                    $_.contentBytes = -join ($_.contentBytes.Substring(0, 10), 'ContentIsTrimmed')
                } else {
                    $_.contentBytes = -join ($_.contentBytes, 'ContentIsTrimmed')
                }
            }
        }
        If ($Message.message.body.content) {
            if ($Message.message.body.content.Length -gt 10) {
                $Message.message.body.content = -join ($Message.message.body.content.Substring(0, 10), 'ContentIsTrimmed')
            } else {
                $Message.message.body.content = -join ($Message.message.body.content, 'ContentIsTrimmed')
            }
        }
        $TrimmedBody = $Message | ConvertTo-Json -Depth 5
        Write-Verbose "Message content: $TrimmedBody"
    }
}
function Send-GraphMailMessageDraft {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [System.Diagnostics.Stopwatch] $StopWatch,
        [string] $MailSentTo,
        [string] $FromField,
        [System.Collections.IDictionary] $Authorization,
        [switch] $Suppress,
        [PSCustomObject] $DraftMessage,
        [switch] $MgGraphRequest

    )
    Try {
        if ($PSCmdlet.ShouldProcess("$MailSentTo", 'Send-EmailMessage')) {
            $Uri = "https://graph.microsoft.com/v1.0/users('" + $FromField + "')/messages/" + $DraftMessage.id + "/send"
            if ($MgGraphRequest) {
                $null = Invoke-MgGraphRequest -Method POST -Uri $Uri -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            } else {
                $null = Invoke-RestMethod -Uri $Uri -Headers $Authorization -Method POST -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop
            }
            if (-not $Suppress) {
                [PSCustomObject] @{
                    Status        = $True
                    Error         = ''
                    SentTo        = $MailSentTo
                    SentFrom      = $FromField
                    Message       = ''
                    TimeToExecute = $StopWatch.Elapsed
                }
            }
        } else {
            if (-not $Suppress) {
                [PSCustomObject] @{
                    Status        = $false
                    Error         = 'Email not sent (WhatIf)'
                    SentTo        = $MailSentTo
                    SentFrom      = $FromField
                    Message       = ''
                    TimeToExecute = $StopWatch.Elapsed
                }
            }
        }
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        }
        $RestError = $_.ErrorDetails.Message
        $RestMessage = $_.Exception.Message
        if ($RestError) {
            try {
                $ErrorMessage = ConvertFrom-Json -InputObject $RestError -ErrorAction Stop
                $ErrorText = $ErrorMessage.error.message
                Write-Warning -Message "Send-GraphMailMessageDraft - Error: $($RestMessage) $($ErrorText)"
            } catch {
                $ErrorText = ''
                Write-Warning -Message "Send-GraphMailMessageDraft - Error: $($RestMessage)"
            }
        } else {
            Write-Warning -Message "Send-GraphMailMessageDraft - Error: $($_.Exception.Message)"
        }
        if ($_.ErrorDetails.RecommendedAction) {
            Write-Warning -Message "Send-GraphMailMessageDraft - Recommended action: $RecommendedAction"
        }
        if (-not $Suppress) {
            [PSCustomObject] @{
                Status        = $False
                Error         = if ($RestError) {
                    "$($RestMessage) $($ErrorText)" 
                } else {
                    $RestMessage 
                }
                SentTo        = $MailSentTo
                SentFrom      = $FromField
                Message       = ''
                TimeToExecute = $StopWatch.Elapsed
            }
        }
    }
}
function Send-SendGridMailMessage {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [object] $From,
        [Array] $To,
        [Array] $Cc,
        [Array] $Bcc,
        [string] $ReplyTo,
        [string] $Subject,
        [alias('Body')][string[]] $HTML,
        [string[]] $Text,
        [alias('Attachments')][string[]] $Attachment,
        [PSCredential] $Credential,
        [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority,
        [switch] $SeparateTo,
        [System.Diagnostics.Stopwatch] $StopWatch,
        [switch] $Suppress
    )
    # https://sendgrid.api-docs.io/v3.0/mail-send/v3-mail-send
    if ($Credential) {
        $AuthorizationData = ConvertFrom-OAuth2Credential -Credential $Credential
    } else {
        return
    }
    $SendGridMessage = [ordered]@{
        personalizations = [System.Collections.Generic.List[object]]::new()
        from             = ConvertTo-SendGridAddress -From $From
        content          = @(
            @{
                type  = if ($HTML) {
                    'text/html' 
                } else {
                    'text/plain' 
                }
                value = if ($HTML) {
                    $HTML 
                } else {
                    $Text 
                }
            }
        )
        attachments      = @(
            foreach ($A in $Attachment) {
                $ItemInformation = Get-Item -LiteralPath $A
                if ($ItemInformation) {
                    $File = [system.io.file]::ReadAllBytes($A)
                    $Bytes = [System.Convert]::ToBase64String($File)
                    @{
                        'filename'    = $ItemInformation.Name
                        #'type' = 'text/plain'
                        'content'     = $Bytes
                        'disposition' = 'attachment' # inline or attachment
                    }
                }
            }
        )
        #send_at = [Math]::Floor([decimal](Get-Date(Get-Date).ToUniversalTime()-UFormat "%s"))
    }

    if ($ReplyTo) {
        $SendGridMessage["reply_to"] = ConvertTo-SendGridAddress -ReplyTo $ReplyTo
    }
    if ($Subject.Length -le 1) {
        # Subject must be at least char in lenght
        $Subject = ' '
    }

    [Array] $SendGridTo = ConvertTo-SendGridAddress -MailboxAddress $To
    [Array] $SendGridCC = ConvertTo-SendGridAddress -MailboxAddress $CC
    [Array] $SendGridBCC = ConvertTo-SendGridAddress -MailboxAddress $Bcc

    if ($SeparateTo) {
        if ($CC -or $BCC) {
            Write-Warning "Send-EmailMessage - Using SeparateTo parameter where there are multiple recipients for TO and CC or BCC is not supported by SendGrid."
            Write-Warning "Send-EmailMessage - SendGrid requires unique email addresses to be available as part of all recipient fields."
            Write-Warning "Send-EmailMessage - Please use SeparateTo parameter only with TO field. Skipping CC/BCC."
        }
        foreach ($T in $To) {
            $Personalization = @{
                subject = $Subject
                to      = @(
                    ConvertTo-SendGridAddress -MailboxAddress $T
                )
            }
            Remove-EmptyValue -Hashtable $Personalization -Recursive
            $SendGridMessage.personalizations.Add($Personalization)
        }
    } else {
        $Personalization = [ordered] @{
            cc      = $SendGridCC
            bcc     = $SendGridBCC
            to      = $SendGridTo
            subject = $Subject
        }
        Remove-EmptyValue -Hashtable $Personalization -Recursive
        $SendGridMessage.personalizations.Add($Personalization)
    }

    Remove-EmptyValue -Hashtable $SendGridMessage -Recursive -Rerun 2

    $InvokeRestMethodParams = [ordered] @{
        URI         = 'https://api.sendgrid.com/v3/mail/send'
        Headers     = @{'Authorization' = "Bearer $($AuthorizationData.Token)" }
        Method      = 'POST'
        Body        = $SendGridMessage | ConvertTo-Json -Depth 5
        ErrorAction = 'Stop'
        ContentType = 'application/json; charset=utf-8'
    }

    [Array] $MailSentTo = ($SendGridTo.Email, $SendGridCC.Email, $SendGridBCC.Email) | ForEach-Object { if ($_) {
            $_ 
        } }
    [string] $MailSentList = $MailSentTo -join ','
    try {
        if ($PSCmdlet.ShouldProcess("$MailSentList", 'Send-EmailMessage')) {
            $null = Invoke-RestMethod @InvokeRestMethodParams
            if (-not $Suppress) {
                [PSCustomObject] @{
                    Status        = $True
                    Error         = ''
                    SentTo        = $MailSentList
                    SentFrom      = $SendGridMessage.From.Email
                    Message       = ''
                    TimeToExecute = $StopWatch.Elapsed
                }
            }
        }
    } catch {
        # This tries to help user with some assesment
        if ($MailSentTo.Count -gt ($MailSentTo | Sort-Object -Unique).Count) {
            $ErrorDetails = ' Addresses in TO/CC/BCC fields must be unique across all fields which may be reason for a failure.'
        } else {
            $ErrorDetails = ''
        }
        # And here we process error
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            throw
        } else {
            Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message) $ErrorDetails"
        }
        if (-not $Suppress) {
            [PSCustomObject] @{
                Status        = $False
                Error         = -join ( $($_.Exception.Message), $ErrorDetails)
                SentTo        = $MailSentTo
                SentFrom      = $SendGridMessage.From.Email
                Message       = ''
                TimeToExecute = $StopWatch.Elapsed
            }
        }
    }
    # This is to make sure data doesn't flood with attachments content
    if ($VerbosePreference) {
        # Trims attachments content
        if ($SendGridMessage.attachments) {
            $SendGridMessage.attachments | ForEach-Object {
                if ($_.content.Length -ge 10) {
                    $_.content = -join ($_.content.Substring(0, 10), 'ContentIsTrimmed')
                } else {
                    $_.content = -join ($_.content, 'ContentIsTrimmed')
                }
            }
        }
        # Trims body content
        If ($SendGridMessage.content.value) {
            if ($SendGridMessage.content[0].value.Length -gt 10) {
                $SendGridMessage.content[0].value = -join ($SendGridMessage.content[0].value.Substring(0, 10), 'ContentIsTrimmed')
            } else {
                $SendGridMessage.content[0].value = -join ($SendGridMessage.content[0].value, 'ContentIsTrimmed')
            }
        }
        $TrimmedBody = $SendGridMessage | ConvertTo-Json -Depth 5
        Write-Verbose "Message content: $TrimmedBody"
    }
}
function Wait-Task {
    # await replacement
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)] $Task
    )
    # https://stackoverflow.com/questions/51218257/await-async-c-sharp-method-from-powershell
    process {
        while (-not $Task.AsyncWaitHandle.WaitOne(200)) { 
        }
        $Task.GetAwaiter().GetResult()
    }
}
function Connect-IMAP {
    [cmdletBinding(DefaultParameterSetName = 'Credential')]
    param(
        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(Mandatory)][string] $Server,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [int] $Port = '993',

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [switch] $SkipCertificateRevocation,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [switch] $SkipCertificateValidation,

        [Parameter(ParameterSetName = 'ClearText', Mandatory)][string] $UserName,
        [Parameter(ParameterSetName = 'ClearText', Mandatory)][string] $Password,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')][System.Management.Automation.PSCredential] $Credential,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [MailKit.Security.SecureSocketOptions] $Options = [MailKit.Security.SecureSocketOptions]::Auto,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [int] $TimeOut = 120000,

        [Parameter(ParameterSetName = 'oAuth2')]
        [switch] $oAuth2
    )
    $Client = [MailKit.Net.Imap.ImapClient]::new()
    try {
        $Client.Connect($Server, $Port, $Options)
    } catch {
        Write-Warning "Connect-IMAP - Unable to connect $($_.Exception.Message)"
        return
    }

    if ($SkipCertificateRevocation) {
        $Client.CheckCertificateRevocation = $false
    }
    if ($SkipCertificateValidation) {
        $Client.ServerCertificateValidationCallback = { $true }
    }
    <#
    void Connect(string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(System.Net.Sockets.Socket socket, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(System.IO.Stream stream, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(uri uri, System.Threading.CancellationToken cancellationToken)
    void Connect(string host, int port, bool useSsl, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(string host, int port, bool useSsl, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(System.Net.Sockets.Socket socket, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(System.IO.Stream stream, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    #>

    if ($Client.TimeOut -ne $TimeOut) {
        $Client.TimeOut = $Timeout
    }
    if ($Client.IsConnected) {
        if ($oAuth2.IsPresent) {
            $Authorization = ConvertFrom-OAuth2Credential -Credential $Credential
            $SaslMechanismOAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($Authorization.UserName, $Authorization.Token)
            try {
                $Client.Authenticate($SaslMechanismOAuth2)
            } catch {
                Write-Warning "Connect-POP - Unable to authenticate via oAuth $($_.Exception.Message)"
                return
            }
        } elseif ($UserName -and $Password) {
            try {
                $Client.Authenticate($UserName, $Password)
            } catch {
                Write-Warning "Connect-IMAP - Unable to authenticate $($_.Exception.Message)"
                return
            }
        } else {
            try {
                $Client.Authenticate($Credential)
            } catch {
                Write-Warning "Connect-IMAP - Unable to authenticate $($_.Exception.Message)"
                return
            }
        }
    } else {
        return
    }
    if ($Client.IsAuthenticated) {
        [ordered] @{
            Uri                      = $Client.SyncRoot.Uri                      #: pops: / / pop.gmail.com:995 /
            AuthenticationMechanisms = $Client.SyncRoot.AuthenticationMechanisms #: { }
            Capabilities             = $Client.SyncRoot.Capabilities             #: Expire, LoginDelay, Pipelining, ResponseCodes, Top, UIDL, User
            Stream                   = $Client.SyncRoot.Stream                   #: MailKit.Net.Pop3.Pop3Stream
            State                    = $Client.SyncRoot.State                    #: Transaction
            IsConnected              = $Client.SyncRoot.IsConnected              #: True
            ApopToken                = $Client.SyncRoot.ApopToken                #:
            ExpirePolicy             = $Client.SyncRoot.ExpirePolicy             #: 0
            Implementation           = $Client.SyncRoot.Implementation           #:
            LoginDelay               = $Client.SyncRoot.LoginDelay               #: 300
            IsAuthenticated          = $Client.IsAuthenticated
            IsSecure                 = $Client.IsSecure
            Data                     = $Client
            Count                    = $Client.Count
        }
    }
    <#
    void Authenticate(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationToken)
    void Authenticate(string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    #>


    <#
    -------------------
    System.Threading.Tasks.Task AuthenticateAsync(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellati
    onToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationT
    oken)
    System.Threading.Tasks.Task AuthenticateAsync(string userName, string password, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationTok
    en cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken
    cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(string userName, string password, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    #>


    #$Client.GetMessageSizes
}
function Connect-oAuthGoogle {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $GmailAccount,
        [Parameter(Mandatory)][string] $ClientID,
        [Parameter(Mandatory)][string] $ClientSecret,
        [ValidateSet("https://mail.google.com/")][string[]] $Scope = @("https://mail.google.com/")
    )

    $ClientSecrets = [Google.Apis.Auth.OAuth2.ClientSecrets]::new()
    $ClientSecrets.ClientId = $ClientID
    $ClientSecrets.ClientSecret = $ClientSecret

    $Initializer = [Google.Apis.Auth.OAuth2.Flows.GoogleAuthorizationCodeFlow+Initializer]::new()
    $Initializer.DataStore = [Google.Apis.Util.Store.FileDataStore]::new("CredentialCacheFolder", $false)
    $Initializer.Scopes = $Scope
    $Initializer.ClientSecrets = $ClientSecrets

    $CodeFlow = [Google.Apis.Auth.OAuth2.Flows.GoogleAuthorizationCodeFlow]::new($Initializer)

    $codeReceiver = [Google.Apis.Auth.OAuth2.LocalServerCodeReceiver]::new()
    $AuthCode = [Google.Apis.Auth.OAuth2.AuthorizationCodeInstalledApp]::new($CodeFlow, $codeReceiver)
    $Credential = $AuthCode.AuthorizeAsync($GmailAccount, [System.Threading.CancellationToken]::None) | Wait-Task

    if ($Credential.Token.IsExpired([Google.Apis.Util.SystemClock]::Default)) {
        $credential.RefreshTokenAsync([System.Threading.CancellationToken]::None) | Wait-Task
    }
    #$oAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($credential.UserId, $credential.Token.AccessToken)
    #$oAuth2
    #[PSCustomObject] @{
    # UserName = $Credential.UserId
    # Token = $Credential.Token.AccessToken
    #}
    ConvertTo-OAuth2Credential -UserName $Credential.UserId -Token $Credential.Token.AccessToken
}

function Connect-oAuthO365 {
    [cmdletBinding()]
    param(
        [string] $Login,
        [Parameter(Mandatory)][string] $ClientID,
        [Parameter(Mandatory)][string] $TenantID,
        [uri] $RedirectUri = 'https://login.microsoftonline.com/common/oauth2/nativeclient',
        [ValidateSet(
            "email",
            "offline_access",
            "https://outlook.office.com/IMAP.AccessAsUser.All",
            "https://outlook.office.com/POP.AccessAsUser.All",
            "https://outlook.office.com/SMTP.Send"
        )][string[]] $Scopes = @(
            "email",
            "offline_access",
            "https://outlook.office.com/IMAP.AccessAsUser.All",
            "https://outlook.office.com/POP.AccessAsUser.All",
            "https://outlook.office.com/SMTP.Send"
        )
    )
    $Options = [Microsoft.Identity.Client.PublicClientApplicationOptions]::new()
    $Options.ClientId = $ClientID
    $Options.TenantId = $TenantID
    $Options.RedirectUri = $RedirectUri

    try {
        $PublicClientApplication = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::CreateWithApplicationOptions($Options).Build()
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else {
            Write-Warning "Connect-oAuthO365 - Error: $($_.Exception.Message)"
            return
        }
    }

    # https://www.powershellgallery.com/packages/MSAL.PS/4.2.1.1/Content/Get-MsalToken.ps1
    # Here we should implement something for Silent Token
    # $Account = $Account
    # $AuthToken = $PublicClientApplication.AcquireTokenSilent($Scopes, $login).ExecuteAsync([System.Threading.CancellationToken]::None) | Wait-Task
    # $oAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($AuthToken.Account.Username, $AuthToken.AccessToken)

    # https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
    try {
        if ($Login) {
            $AuthToken = $PublicClientApplication.AcquireTokenInteractive($Scopes).ExecuteAsync([System.Threading.CancellationToken]::None) | Wait-Task
        } else {
            $AuthToken = $PublicClientApplication.AcquireTokenInteractive($Scopes).WithLoginHint($Login).ExecuteAsync([System.Threading.CancellationToken]::None) | Wait-Task
        }
        # Here we should save the AuthToken.Account somehow, somewhere
        # $AuthToken.Account | Export-Clixml -Path $Env:USERPROFILE\Desktop\test.xml -Depth 2
        #[PSCustomObject] @{
        # UserName = $AuthToken.Account.UserName
        # Token = $AuthToken.AccessToken
        #}
        ConvertTo-OAuth2Credential -UserName $AuthToken.Account.UserName -Token $AuthToken.AccessToken

        #$oAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($AuthToken.Account.Username, $AuthToken.AccessToken)
        #$oAuth2
    } catch {
        Write-Warning "Connect-oAuth - $_"
    }
}
function Connect-POP {
    [alias('Connect-POP3')]
    [cmdletBinding()]
    param(
        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(Mandatory)][string] $Server,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [int] $Port = '995',

        [Parameter(ParameterSetName = 'ClearText', Mandatory)][string] $UserName,
        [Parameter(ParameterSetName = 'ClearText', Mandatory)][string] $Password,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [switch] $SkipCertificateRevocation,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [switch] $SkipCertificateValidation,

        [Parameter(ParameterSetName = 'oAuth2', Mandatory)]
        [Parameter(ParameterSetName = 'Credential')][System.Management.Automation.PSCredential] $Credential,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [MailKit.Security.SecureSocketOptions] $Options = [MailKit.Security.SecureSocketOptions]::Auto,

        [Parameter(ParameterSetName = 'oAuth2')]
        [Parameter(ParameterSetName = 'Credential')]
        [Parameter(ParameterSetName = 'ClearText')]
        [int] $TimeOut = 120000,

        [Parameter(ParameterSetName = 'oAuth2')]
        [switch] $oAuth2
    )
    $Client = [MailKit.Net.Pop3.Pop3Client]::new()
    try {
        $Client.Connect($Server, $Port, $Options)
    } catch {
        Write-Warning "Connect-POP - Unable to connect $($_.Exception.Message)"
        return
    }

    if ($SkipCertificateRevocation) {
        $Client.CheckCertificateRevocation = $false
    }
    if ($SkipCertificateValidation) {
        $Client.ServerCertificateValidationCallback = { $true }
    }

    <#
    void Connect(string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(System.Net.Sockets.Socket socket, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(System.IO.Stream stream, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void Connect(uri uri, System.Threading.CancellationToken cancellationToken)
    void Connect(string host, int port, bool useSsl, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(string host, int port, bool useSsl, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(System.Net.Sockets.Socket socket, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    void IMailService.Connect(System.IO.Stream stream, string host, int port, MailKit.Security.SecureSocketOptions options, System.Threading.CancellationToken cancellationToken)
    #>

    if ($Client.TimeOut -ne $TimeOut) {
        $Client.TimeOut = $Timeout
    }
    if ($Client.IsConnected) {
        if ($oAuth2.IsPresent) {
            $Authorization = ConvertFrom-OAuth2Credential -Credential $Credential
            $SaslMechanismOAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($Authorization.UserName, $Authorization.Token)
            try {
                $Client.Authenticate($SaslMechanismOAuth2)
            } catch {
                Write-Warning "Connect-POP - Unable to authenticate via oAuth $($_.Exception.Message)"
                return
            }
        } elseif ($UserName -and $Password) {
            try {
                $Client.Authenticate($UserName, $Password)
            } catch {
                Write-Warning "Connect-POP - Unable to authenticate via UserName/Password $($_.Exception.Message)"
                return
            }
        } else {
            try {
                $Client.Authenticate($Credential)
            } catch {
                Write-Warning "Connect-POP - Unable to authenticate via Credentials $($_.Exception.Message)"
                return
            }
        }
    } else {
        return
    }
    if ($Client.IsAuthenticated) {
        [ordered] @{
            Uri                      = $Client.SyncRoot.Uri                      #: pops: / / pop.gmail.com:995 /
            AuthenticationMechanisms = $Client.SyncRoot.AuthenticationMechanisms #: { }
            Capabilities             = $Client.SyncRoot.Capabilities             #: Expire, LoginDelay, Pipelining, ResponseCodes, Top, UIDL, User
            Stream                   = $Client.SyncRoot.Stream                   #: MailKit.Net.Pop3.Pop3Stream
            State                    = $Client.SyncRoot.State                    #: Transaction
            IsConnected              = $Client.SyncRoot.IsConnected              #: True
            ApopToken                = $Client.SyncRoot.ApopToken                #:
            ExpirePolicy             = $Client.SyncRoot.ExpirePolicy             #: 0
            Implementation           = $Client.SyncRoot.Implementation           #:
            LoginDelay               = $Client.SyncRoot.LoginDelay               #: 300
            IsAuthenticated          = $Client.IsAuthenticated
            IsSecure                 = $Client.IsSecure
            Data                     = $Client
            Count                    = $Client.Count
        }
    }
    <#
    void Authenticate(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void Authenticate(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationToken)
    void Authenticate(string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(string userName, string password, System.Threading.CancellationToken cancellationToken)
    void IMailService.Authenticate(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    #>


    <#
    -------------------
    System.Threading.Tasks.Task AuthenticateAsync(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationToken cancellati
    onToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task AuthenticateAsync(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken cancellationT
    oken)
    System.Threading.Tasks.Task AuthenticateAsync(string userName, string password, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Net.ICredentials credentials, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Text.Encoding encoding, System.Net.ICredentials credentials, System.Threading.CancellationTok
    en cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(System.Text.Encoding encoding, string userName, string password, System.Threading.CancellationToken
    cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(string userName, string password, System.Threading.CancellationToken cancellationToken)
    System.Threading.Tasks.Task IMailService.AuthenticateAsync(MailKit.Security.SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken)
    #>


    #$Client.GetMessageSizes
}
function ConvertFrom-EmlToMsg {
    <#
    .SYNOPSIS
    Convert .eml file to .msg file
 
    .DESCRIPTION
    Convert .eml file to .msg file
 
    .PARAMETER InputPath
    Path to the .eml file
 
    .PARAMETER OutputPath
    Path to the .msg file
 
    .EXAMPLE
    ConvertFrom-EmlToMsg -InputPath 'C:\Temp\Sample.eml' -OutputPath 'C:\Temp\Sample.msg'
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    param(
        [parameter(Mandatory)][alias('FilePath')][string] $InputPath,
        [parameter(Mandatory)][string] $OutputPath
    )
    if ($InputPath -and (Test-Path -LiteralPath $InputPath)) {
        try {
            [MsgKit.Converter]::ConvertEmlToMsg($InputPath, $OutputPath)
            [PSCustomObject] @{
                Status       = $true
                InputPath    = $InputPath
                OutputPath   = $OutputPath
                ErrorMessage = ''
            }
        } catch {
            Write-Warning -Message "ConvertFrom-EmlToMsg - Error converting $FilePath to $OutputPath. Error: $($_.Exception.Message)"
            [PSCustomObject] @{
                Status       = $false
                InputPath    = $InputPath
                OutputPath   = $OutputPath
                ErrorMessage = $_.Exception.Message
            }
        }
    } else {
        Write-Warning -Message "ConvertFrom-EmlToMsg - File $FilePath doesn't exist."
        [PSCustomObject] @{
            Status       = $false
            InputPath    = $InputPath
            OutputPath   = $OutputPath
            ErrorMessage = $false
        }
    }
}
function ConvertTo-GraphCredential {
    [cmdletBinding(DefaultParameterSetName = 'ClearText')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [Parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [string] $ClientID,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [string] $ClientSecret,
        [Parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [string] $ClientSecretEncrypted,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [Parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [string] $DirectoryID,
        [Parameter(Mandatory, ParameterSetName = 'MsalToken')][alias('Token')][string] $MsalToken,
        [Parameter(Mandatory, ParameterSetName = 'MsalTokenEncrypted')][alias('TokenEncrypted')][string] $MsalTokenEncrypted
    )
    if ($MsalToken -or $MsalTokenEncrypted) {
        # Convert to SecureString
        Try {
            if ($MsalTokenEncrypted) {
                $EncryptedToken = ConvertTo-SecureString -String $MsalTokenEncrypted -ErrorAction Stop
            } else {
                $EncryptedToken = ConvertTo-SecureString -String $MsalToken -AsPlainText -Force -ErrorAction Stop
            }
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else {
                Write-Warning "ConvertTo-GraphCredential - Error: $($_.Exception.Message)"
                return
            }
        }
        $UserName = 'MSAL'
        $EncryptedCredentials = [System.Management.Automation.PSCredential]::new($UserName, $EncryptedToken)
        $EncryptedCredentials
    } else {
        # Convert to SecureString
        Try {
            if ($ClientSecretEncrypted) {
                $EncryptedToken = ConvertTo-SecureString -String $ClientSecretEncrypted -ErrorAction Stop
            } else {
                $EncryptedToken = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force -ErrorAction Stop
            }
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else {
                Write-Warning "ConvertTo-GraphCredential - Error: $($_.Exception.Message)"
                return
            }
        }
        $UserName = -join ($ClientID, '@', $DirectoryID)
        $EncryptedCredentials = [System.Management.Automation.PSCredential]::new($UserName, $EncryptedToken)
        $EncryptedCredentials
    }
}
function ConvertTo-OAuth2Credential {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $UserName,
        [Parameter(Mandatory)][string] $Token
    )
    # Convert to SecureString
    $EncryptedToken = ConvertTo-SecureString -String $Token -AsPlainText -Force
    $EncryptedCredentials = [System.Management.Automation.PSCredential]::new($UserName, $EncryptedToken)
    $EncryptedCredentials
}
function ConvertTo-SendGridCredential {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ApiKey
    )
    # Convert to SecureString
    $EncryptedToken = ConvertTo-SecureString -String $ApiKey -AsPlainText -Force
    $EncryptedCredentials = [System.Management.Automation.PSCredential]::new('SendGrid', $EncryptedToken)
    $EncryptedCredentials
}
function Disconnect-IMAP {
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $Client
    )
    if ($Client.Data) {
        try {
            $Client.Data.Disconnect($true)
        } catch {
            Write-Warning "Disconnect-IMAP - Unable to authenticate $($_.Exception.Message)"
            return
        }
    }
}
function Disconnect-POP {
    [alias('Disconnect-POP3')]
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $Client
    )
    if ($Client.Data) {
        try {
            $Client.Data.Disconnect($true)
        } catch {
            Write-Warning "Disconnect-POP - Unable to authenticate $($_.Exception.Message)"
            return
        }
    }
}
function Find-BIMIRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide BIMI information
 
    .DESCRIPTION
    Queries DNS to provide BIMI information
 
    .PARAMETER DomainName
    Name/DomainName to query for BIMI record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    Find-BIMIRecord -DomainName 'mxtoolbox.com' -DNSProvider Google | Format-Table
 
    .EXAMPLE
    Find-BIMIRecord -DomainName 'google.com' -DNSProvider Cloudflare | Format-Table
 
    .EXAMPLE
    Find-BIMIRecord -DomainName 'mxtoolbox.com' | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning -Message 'Find-BIMIRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "default._bimi.$D"
                Type        = 'TXT'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                [Array] $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'v=BIMI1;'
                if (-not $AsObject) {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive -join '; '
                        BIMI        = $DNSRecordAnswers.Text -join '; '
                        QueryServer = $DNSRecord.NameServer -join '; '
                    }
                } else {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive
                        BIMI        = $DNSRecordAnswers.Text
                        QueryServer = $DNSRecord.NameServer
                    }
                }
            } catch {
                $MailRecord = [ordered] @{
                    Name        = $D
                    Count       = 0
                    TimeToLive  = ''
                    BIMI        = ''
                    QueryServer = ''
                }
                Write-Warning -Message "Find-BIMIRecord - $_"
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}


function Find-CAARecord {
    <#
    .SYNOPSIS
    Queries DNS to provide CAA information
 
    .DESCRIPTION
    Queries DNS to provide CAA information
 
    .PARAMETER DomainName
    Name/DomainName to query for CAA record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .EXAMPLE
    Find-CAARecord -DomainName "evotec.pl" -DNSProvider Cloudflare | Format-Table -AutoSize
    Find-CAARecord -DomainName 'evotec.pl' -DNSProvider Google | Format-Table -AutoSize
    Find-CAARecord -DomainName 'evotec.pl' -Verbose | Format-Table -AutoSize
 
    .EXAMPLE
    Find-CAARecord -DomainName "mbank.pl" -DNSProvider Cloudflare | Format-Table -AutoSize
    Find-CAARecord -DomainName 'mbank.pl' -DNSProvider Google | Format-Table -AutoSize
    Find-CAARecord -DomainName 'mbank.pl' -Verbose | Format-Table -AutoSize
 
    .NOTES
    We try to follow rfc, but the key/value pair may not be correct. If you find any issues, please report them.
 
    The flag must be a number between 0 and 255, 0 being the most commonly used value.
    The tag must be one of issue, issuewild, or iodef.
    The value part:
    - It must be wrapped between double quotes ".
    - There are no length restrictions on this part.
    - Any inner double quotes " must be escaped with the \" character sequence.
    - Based on the specific tag value, it must follow the extra rules described below:
 
    issue and issuewild tag value
    - It must contain a domain name.
    - The domain name can be followed by a list of parameters with the following pattern:
 
    - 0 issue "letsencrypt.com;key1=value1;key2=value2"
    - The domain name can also be left empty, which must be indicated providing just ";" as a value:
 
    - 0 issue ";"
    iodef tag value
    - It must contain a URL.
    - The provided URL must have one of the following schemes: mailto, http, or https.
    - If the URL has the mailto scheme, it must conform to an email URL, like mailto:admin@example.com.
    - If the URL has the http or https schemes, it must be a valid HTTP/HTTPS URL, like https://dnsimple.com/report_caa.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning -Message 'Find-CAARecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "$D"
                Type        = 'CAA'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All -Verbose
                }
                [Array] $DNSRecordAnswers = $DNSRecord.Answers
                foreach ($Record in $DNSRecordAnswers) {
                    if ($DNSProvider -eq 'Cloudflare') {
                        $Data = Convert-CloudflareToGoogleCAA -hexString $Record.Text
                        $NewData = $Data -split ' '
                        $Flags = $NewData[0]
                        $Tag = $NewData[1]
                        $DomainValue = $NewData[2].Replace('"', '').Replace(';', '')
                        $cansignhttpexchanges = $false
                        if ($NewData[3] -match 'cansignhttpexchanges') {
                            $cansignhttpexchangesTemp = $NewData[3] -replace 'cansignhttpexchanges=', ''
                            if ($cansignhttpexchangesTemp -eq 'yes') {
                                $cansignhttpexchanges = $true
                            } else {
                                $cansignhttpexchanges = $false
                            }
                        }
                        [PSCustomObject] @{
                            DomainName           = $D
                            Flags                = $Flags
                            Tag                  = $Tag
                            DomainValue          = $DomainValue
                            CanSignHttpExchanges = $cansignhttpexchanges
                            TimeToLive           = $Record.TimeToLive
                        }
                    } elseif ($DNSProvider -eq 'Google') {
                        $Data = $Record.Text
                        $NewData = $Data -split ' '
                        $Flags = $NewData[0]
                        $Tag = $NewData[1]
                        $DomainValue = $NewData[2].Replace('"', '').Replace(';', '')
                        $cansignhttpexchanges = $false
                        if ($NewData[3] -match 'cansignhttpexchanges') {
                            $cansignhttpexchangesTemp = $NewData[3] -replace 'cansignhttpexchanges=', ''
                            if ($cansignhttpexchangesTemp -eq 'yes') {
                                $cansignhttpexchanges = $true
                            } else {
                                $cansignhttpexchanges = $false
                            }
                        }
                        [PSCustomObject] @{
                            DomainName           = $D
                            Flags                = $Flags
                            Tag                  = $Tag
                            DomainValue          = $DomainValue
                            CanSignHttpExchanges = $cansignhttpexchanges
                            TimeToLive           = $Record.TimeToLive
                        }
                    } else {
                        $Flags = $Record.Flags
                        $Tag = $Record.Tag
                        $Data = $Record.Value.Replace('\;', ';')
                        $NewData = $Data -split ';'
                        $DomainValue = $NewData[0].Trim()
                        # digicert.com\; cansignhttpexchanges=yes
                        if ($NewData[1] -match 'cansignhttpexchanges') {
                            $cansignhttpexchangesTemp = $NewData[1].Trim() -replace 'cansignhttpexchanges=', ''
                            if ($cansignhttpexchangesTemp -eq 'yes') {
                                $cansignhttpexchanges = $true
                            } else {
                                $cansignhttpexchanges = $false
                            }
                        } else {
                            $cansignhttpexchanges = $false
                        }
                        [PSCustomObject] @{
                            DomainName           = $D
                            Flags                = $Flags
                            Tag                  = $Tag
                            DomainValue          = $DomainValue
                            CanSignHttpExchanges = $cansignhttpexchanges
                            TimeToLive           = $Record.TimeToLive
                        }
                    }
                }
                # if there are no records we return empty object
                if ($DNSRecordAnswers.Count -eq 0) {
                    [PSCustomObject] @{
                        DomainName           = $D
                        Flags                = ''
                        Tag                  = ''
                        DomainValue          = ''
                        CanSignHttpExchanges = $null
                        TimeToLive           = $null
                    }
                }
            } catch {
                [PSCustomObject] @{
                    DomainName           = $D
                    Flags                = ''
                    Tag                  = ''
                    DomainValue          = ''
                    CanSignHttpExchanges = $null
                    TimeToLive           = $null
                }
                Write-Warning -Message "Find-CAARecord - Error: $($_.Exception.Message)"
            }
        }
    }
}
function Find-DANERecord {
    <#
    .SYNOPSIS
    Queries DNS to provide DANE information
 
    .DESCRIPTION
    Queries DNS to provide DANE information
 
    .PARAMETER DomainName
    Name/DomainName to query for DANE record (converts to MX record automatically)
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    Find-DaneRecord -DomainName 'ietf.org' -DNSProvider Google | Format-Table
 
    .EXAMPLE
    Find-DaneRecord -DomainName 'ietf.org' -DNSProvider Cloudflare | Format-Table
 
    .EXAMPLE
    Find-DaneRecord -DomainName 'evotec.xyz' | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    begin {
        $MxRecords = Find-MxRecord -DomainName $DomainName
    }
    process {
        If (-not $MxRecords.MX) {
            $MailRecord = [ordered] @{
                Name                       = $MXRecords.Name
                Status                     = $false
                Count                      = 0
                Record                     = ''
                TheCertificateUsageField   = ''
                SelectorField              = ''
                MatchingTypeField          = ''
                CertificateAssociationData = ''
                DANE                       = ''
                TimeToLive                 = ''
                QueryServer                = ''
            }
            Write-Warning -Message "Find-DANERecord - Unable to found MX record for '$($MXRecords.Name)'"
        } else {
            foreach ($Domain in $MxRecords.MX) {
                $D = $Domain
                $Splat = @{
                    Name        = "_25._tcp.$D"
                    Type        = 'TLSA'
                    ErrorAction = 'Stop'
                }
                try {
                    if ($DNSProvider) {
                        $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                    } else {
                        if ($DnsServer) {
                            $Splat['Server'] = $DnsServer
                        }
                        $DNSRecord = Resolve-DnsQuery @Splat -All
                    }
                    [Array] $DNSRecordAnswers = $DNSRecord.Answers
                    if ($DNSRecordAnswers.Count -eq 1) {
                        if ($DNSRecordAnswers.Text) {
                            if ($DNSProvider -eq 'Cloudflare') {
                                $DataToAnalyze = Convert-CloudFlareToGoogleDane -DaneRecord $DNSRecordAnswers.Text
                                $Record = $DNSRecordAnswers.Name
                            } else {
                                $DataToAnalyze = $DNSRecordAnswers.Text
                                $Record = $DNSRecordAnswers.Name
                            }
                            $DANEData = $DataToAnalyze -split " "
                            $DANEData = @(
                                # we are translating data to be similar to DNSClient output
                                [DnsClient.Protocol.TlsaCertificateUsage] $DANEData[0]
                                [DnsClient.Protocol.TlsaSelector] $DANEData[1]
                                [DnsClient.Protocol.TlsaMatchingType] $DANEData[2]
                                $DANEData[3]
                            )
                        } else {
                            # Proper DANE record delivered by DNSClient contains translation of the data
                            $DANEData = @(
                                $DNSRecordAnswers[0].CertificateUsage,
                                $DNSRecordAnswers[0].Selector,
                                $DNSRecordAnswers[0].MatchingType,
                                $DNSRecordAnswers[0].CertificateAssociationDataAsString
                            )
                            $Record = $DNSRecordAnswers.DomainName
                        }
                    } else {
                        $DANEData = @()
                        $Record = $DNSRecordAnswers.Name
                    }
                    # https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities
                    if (-not $AsObject) {
                        $MailRecord = [ordered] @{
                            Name                       = $D
                            Status                     = if ($DANEData.Count -eq 4) {
                                $true 
                            } else {
                                $false 
                            }
                            Count                      = $DNSRecordAnswers.Count
                            Record                     = $Record

                            # Certificate usage
                            # The first field after the TLSA text in the DNS RR, specifies how to verify the certificate.
                            # A value of 0 is for what is commonly called CA constraint (and PKIX-TA). The certificate provided when establishing TLS must be issued by the listed root-CA or one of its intermediate CAs, with a valid certification path to a root-CA already trusted by the application doing the verification. The record may just point to an intermediate CA, in which case the certificate for this service must come via this CA, but the entire chain to a trusted root-CA must still be valid.[a]
                            # A value of 1 is for what is commonly called service certificate constraint (and PKIX-EE). The certificate used must match the TLSA record, and it must also pass PKIX certification path validation to a trusted root-CA.
                            # A value of 2 is for what is commonly called trust anchor assertion (and DANE-TA). The TLSA record matches the certificate of the root CA, or one of the intermediate CAs, of the certificate in use by the service. The certification path must be valid up to the matching certificate, but there is no need for a trusted root-CA.
                            # A value of 3 is for what is commonly called domain issued certificate (and DANE-EE). The TLSA record matches the used certificate itself. The used certificate does not need to be signed by other parties. This is useful for self-signed certificates, but also for cases where the validator does not have a list of trusted root certificates.

                            CertificateUsage           = $DANEData[0]

                            # Selector
                            # When connecting to the service and a certificate is received, the selector field specifies which parts of it should be checked.
                            # A value of 0 means to select the entire certificate for matching.
                            # A value of 1 means to select just the public key for certificate matching. Matching the public key is often sufficient, as this is likely to be unique.
                            SelectorField              = $DANEData[1]

                            # Matching type
                            # A type of 0 means the entire information selected is present in the certificate association data.
                            # A type of 1 means to do a SHA - 256 hash of the selected data.
                            # A type of 2 means to do a SHA - 512 hash of the selected data.
                            MatchingTypeField          = $DANEData[2]

                            # The actual data to be matched given the settings of the other fields. This is a long "text string" of hexadecimal data.
                            CertificateAssociationData = if ($DANEData[3]) {
                                $DANEData[3].ToLower() 
                            } else {
                                '' 
                            }
                            DANE                       = $DNSRecordAnswers.Text -join '; '
                            TimeToLive                 = $DNSRecordAnswers.TimeToLive -join '; '
                            QueryServer                = $DNSRecord.NameServer -join '; '
                        }
                    } else {
                        $MailRecord = [ordered] @{
                            Name                       = $D
                            Status                     = if ($DANEData.Count -eq 4) {
                                $true 
                            } else {
                                $false 
                            }
                            Count                      = $DNSRecordAnswers.Count
                            Record                     = $DNSRecordAnswers.Name
                            CertificateUsage           = $DANEData[0]
                            SelectorField              = $DANEData[1]
                            MatchingTypeField          = $DANEData[2]
                            CertificateAssociationData = $DANEData[3]
                            DANE                       = $DNSRecordAnswers.Text
                            TimeToLive                 = $DNSRecordAnswers.TimeToLive
                            QueryServer                = $DNSRecord.NameServer
                        }
                    }
                } catch {
                    $MailRecord = [ordered] @{
                        Name                       = $D
                        Status                     = $false
                        Count                      = 0
                        Record                     = ''
                        TheCertificateUsageField   = ''
                        SelectorField              = ''
                        MatchingTypeField          = ''
                        CertificateAssociationData = ''
                        DANE                       = ''
                        TimeToLive                 = ''
                        QueryServer                = ''
                    }
                    Write-Warning -Message "Find-DANERecord - $_"
                }
            }
        }
        if ($AsHashTable) {
            $MailRecord
        } else {
            [PSCustomObject] $MailRecord
        }
    }
}
function Find-DKIMRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide DKIM information
 
    .DESCRIPTION
    Queries DNS to provide DKIM information
 
    .PARAMETER DomainName
    Name/DomainName to query for DKIM record
 
    .PARAMETER Selector
    Selector name. Default: selector1
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    # Standard way
    Find-DKIMRecord -DomainName 'evotec.pl', 'evotec.xyz' | Format-Table *
 
    .EXAMPLE
    # Https way via Cloudflare
    Find-DKIMRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Cloudflare | Format-Table *
 
    .EXAMPLE
    # Https way via Google
    Find-DKIMRecord -DomainName 'evotec.pl', 'evotec.xyz' -Selector 'selector1' -DNSProvider Google | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $Selector = 'selector1',
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $S = $Selector
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $S = $Domain.Selector
                $D = $Domain.DomainName
                if (-not $S -and -not $D) {
                    Write-Warning 'Find-DKIMRecord - properties DomainName and Selector are required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "$S._domainkey.$D"
                Type        = 'TXT'
                ErrorAction = 'SilentlyContinue'
            }
            if ($DNSProvider) {
                $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
            } else {
                if ($DnsServer) {
                    $Splat['Server'] = $DnsServer
                }
                $DNSRecord = Resolve-DnsQuery @Splat -All
            }
            $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'DKIM1'
            if (-not $AsObject) {
                $MailRecord = [ordered] @{
                    Name        = $D
                    Count       = $DNSRecordAnswers.Text.Count
                    Selector    = "$D`:$S"
                    DKIM        = $DNSRecordAnswers.Text -join '; '
                    QueryServer = $DNSRecord.NameServer
                }
            } else {
                $MailRecord = [ordered] @{
                    Name        = $D
                    Count       = $DNSRecordAnswers.Text.Count
                    Selector    = "$D`:$S"
                    DKIM        = $DNSRecordAnswers.Text -join '; '
                    QueryServer = $DNSRecord.NameServer -join '; '
                }
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}
function Find-DMARCRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide DMARC information
 
    .DESCRIPTION
    Queries DNS to provide DMARC information
 
    .PARAMETER DomainName
    Name/DomainName to query for DMARC record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    # Standard way
    Find-DMARCRecord -DomainName 'evotec.pl', 'evotec.xyz' | Format-Table *
 
    .EXAMPLE
    # Https way via Cloudflare
    Find-DMARCRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Cloudflare | Format-Table *
 
    .EXAMPLE
    # Https way via Google
    Find-DMARCRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Google | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    begin {
        # $DMARCTagsNames = [ordered] @{
        # 'v' = 'Protocol version'
        # 'p' = 'Policy for the domain'
        # 'sp' = 'Policy for subdomains'
        # 'rua' = 'Reporting URI of aggregate reports'
        # 'ruf' = 'Reporting URI of forensic reports'
        # 'pct' = 'Percentage of messages subjected to filtering'
        # 'adkim' = 'Alignment mode for DKIM'
        # 'aspf' = 'Alignment mode for SPF'
        # 'ri' = 'Reporting interval'
        # 'fo' = 'Failure reporting options'
        # }
        $DMARCTags = [ordered] @{
            'v'     = 'ProtocolVersion'
            'p'     = 'Policy'
            'sp'    = 'SubdomainPolicy'
            'rua'   = 'AggregateReportURI'
            'ruf'   = 'ForensicReportURI'
            'pct'   = 'Percent' # This Value is the percentage of email to be surveyed and reported back to the domain owner.
            'adkim' = 'DKIMAlignmentMode'
            'aspf'  = 'SPFAlignmentMode'
            'ri'    = 'ReportInterval'
            'fo'    = 'FailureOptions'
        }
    }
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning 'Find-DMARCRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "_dmarc.$D"
                Type        = 'TXT'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'DMARC1'
                if (-not $AsObject) {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        #Count = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive -join '; '
                        DMARC       = $DNSRecordAnswers.Text -join '; '
                        QueryServer = $DNSRecord.NameServer -join '; '
                    }
                } else {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        #Count = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive
                        DMARC       = $DNSRecordAnswers.Text
                        QueryServer = $DNSRecord.NameServer
                    }
                }
                $MailRecord['PolicyAdvisory'] = "Domain has NO DMARC record. This domain is at risk to being abused."
                $MailRecord['SubdomainPolicyAdvisory'] = $null

                # Split the DMARC record into an array of settings
                $DMARCSettings = $MailRecord.DMARC -split ';'

                # # Initialize an empty hashtable to store the settings
                $DMARCSettingsHashTable = [ordered] @{}

                # Loop through each setting
                foreach ($setting in $DMARCSettings) {
                    # Split the setting into a key-value pair
                    $KeyValuePair = $setting -split '='
                    $Key = $KeyValuePair[0].Trim()
                    $Value = $KeyValuePair[1]
                    # Translate 's' to 'Strict mode' and 'r' to 'Relaxed mode' for 'adkim' and 'aspf'
                    if ($Key -eq 'adkim' -or $Key -eq 'aspf') {
                        switch ($Value) {
                            's' {
                                $Value = 'Strict mode' 
                            }
                            'r' {
                                $Value = 'Relaxed mode' 
                            }
                        }
                    } elseif ($Key -eq 'fo') {
                        # Translate values for 'fo'
                        $foValues = $Value -split ':'
                        $translatedFoValues = @()
                        foreach ($foValue in $foValues) {
                            switch ($foValue) {
                                '0' {
                                    $translatedFoValues += 'Generate report if all mechanisms fail (0)' 
                                }
                                '1' {
                                    $translatedFoValues += 'Generate report if any mechanism fails (1)' 
                                }
                                'd' {
                                    $translatedFoValues += 'Generate report if DKIM test fails (d)' 
                                }
                                's' {
                                    $translatedFoValues += 'Generate report if SPF test fails (s)' 
                                }
                            }
                        }
                        $Value = $translatedFoValues -join ', '
                    } elseif ($Key -eq 'ri') {
                        $ValueInSeconds = [int]$Value
                        if ($ValueInSeconds -ge 86400 -and $ValueInSeconds % 86400 -eq 0) {
                            $ValueInDays = $ValueInSeconds / 86400
                            $Value = "$ValueInDays day(s)"
                        } elseif ($ValueInSeconds -ge 3600 -and $ValueInSeconds % 3600 -eq 0) {
                            $ValueInHours = $ValueInSeconds / 3600
                            $Value = "$ValueInHours hour(s)"
                        } else {
                            $Value = "$ValueInSeconds second(s)"
                        }
                    }
                    # Add the key-value pair to the hashtable
                    $DMARCSettingsHashTable[$Key] = $Value
                }
                # Check if 'adkim' and 'aspf' are defined in the DMARC record
                if (-not $DMARCSettingsHashTable['adkim']) {
                    # If 'adkim' is not defined, set it to 'Relaxed (default)'
                    $DMARCSettingsHashTable['adkim'] = 'Relaxed (default)'
                }
                if (-not $DMARCSettingsHashTable['aspf']) {
                    # If 'aspf' is not defined, set it to 'Relaxed (default)'
                    $DMARCSettingsHashTable['aspf'] = 'Relaxed (default)'
                }
                # Check if 'pct' is defined in the DMARC record
                if (-not $DMARCSettingsHashTable['pct']) {
                    # If 'pct' is not defined, set it to 100
                    # as per the DMARC specification
                    $DMARCSettingsHashTable['pct'] = '100'
                }
                # Check if 'fo' is defined in the DMARC record
                if ($DMARCSettingsHashTable.Keys -notcontains 'fo') {
                    # If 'fo' is not defined, set it to 'Generate report if all mechanisms fail (default)'
                    $DMARCSettingsHashTable['fo'] = 'Generate report if all mechanisms fail (default - 0)'
                }
                # Check if 'ri' is defined in the DMARC record
                if ($DMARCSettingsHashTable.Keys -notcontains 'ri') {
                    # If 'ri' is not defined, set it to '1 day (default)'
                    $DMARCSettingsHashTable['ri'] = '1 day (default)'
                }
                foreach ($Tag in $DMARCTags.Keys) {
                    # If the tag is in the DMARC record, add its value to the MailRecord
                    if ($DMARCSettingsHashTable[$Tag]) {
                        if ($Tag -eq 'rua' -or $Tag -eq 'ruf') {
                            if ($Tag -eq 'rua') {
                                $MailRecord['AggregateReportEmail'] = $null
                                $MailRecord['AggregateReportHTTP'] = $null
                            } else {
                                $MailRecord['ForensicReportEmail'] = $null
                                $MailRecord['ForensicReportHTTP'] = $null
                            }
                            if ($DMARCSettingsHashTable[$Tag] -match '^mailto:') {
                                # Check if the value starts with 'mailto:'
                                $Key = if ($Tag -eq 'rua') {
                                    'AggregateReportEmail' 
                                } else {
                                    'ForensicReportEmail' 
                                }
                                $MailRecord[$Key] = $DMARCSettingsHashTable[$Tag].Replace('mailto:', '') -split ','
                            } elseif ($DMARCSettingsHashTable[$Tag] -match '^http:') {
                                # Check if the value starts with 'http:'
                                $Key = if ($Tag -eq 'rua') {
                                    'AggregateReportHTTP' 
                                } else {
                                    'ForensicReportHTTP' 
                                }
                                $MailRecord[$Key] = $DMARCSettingsHashTable[$Tag] -split ','
                            }
                        } else {
                            $MailRecord[$DMARCTags[$Tag]] = $DMARCSettingsHashTable[$Tag]
                        }
                    } else {
                        $MailRecord[$DMARCTags[$tag]] = $null
                    }
                }
                switch ($MailRecord.Policy) {
                    'none' {
                        $MailRecord['PolicyAdvisory'] = "Domain has a DMARC policy (p=none), but the DMARC policy does not prevent abuse."
                    }
                    'quarantine' {
                        $MailRecord['PolicyAdvisory'] = "Domain has a DMARC policy (p=quarantine), and will prevent abuse of domain, but finally should be set to p=reject."
                    }
                    'reject' {
                        $MailRecord['PolicyAdvisory'] = "Domain has a DMARC policy (p=reject), and will prevent abuse of domain."
                    }
                }
                switch ($MailRecord.SubdomainPolicy) {
                    'none' {
                        $MailRecord['SubdomainPolicyAdvisory'] = "Subdomain has a DMARC policy (sp=none), but the DMARC policy does not prevent abuse."
                    }
                    'quarantine' {
                        $MailRecord['SubdomainPolicyAdvisory'] = "Subdomain has a DMARC policy (sp=quarantine), and will prevent abuse of domain, but finally should be set to sp=reject."
                    }
                    'reject' {
                        $MailRecord['SubdomainPolicyAdvisory'] = "Subdomain has a DMARC policy (sp=reject), and will prevent abuse of your domain."
                    }
                }
            } catch {
                $MailRecord = [ordered] @{
                    Name        = $D
                    #Count = 0
                    TimeToLive  = ''
                    DMARC       = ''
                    QueryServer = ''
                    Advisory    = "No DMARC record found."
                }
                foreach ($Tag in $DMARCTags.Keys) {
                    if ($Tag -eq 'rua' -or $Tag -eq 'ruf') {
                        if ($Tag -eq 'rua') {
                            $MailRecord['AggregateReportEmail'] = $null
                            $MailRecord['AggregateReportHTTP'] = $null
                        } else {
                            $MailRecord['ForensicReportEmail'] = $null
                            $MailRecord['ForensicReportHTTP'] = $null
                        }
                    } else {
                        $MailRecord[$DMARCTags[$Tag]] = $null
                    }
                }
                Write-Warning "Find-DMARCRecord - Error $_"
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}
function Find-DNSBL {
    <#
    .SYNOPSIS
    Searches DNSBL if particular IP is blocked on DNSBL.
 
    .DESCRIPTION
    Searches DNSBL if particular IP is blocked on DNSBL.
 
    .PARAMETER IP
    IP to check if it exists on DNSBL
 
    .PARAMETER BlockListServers
    Provide your own blocklist of servers
 
    .PARAMETER All
    Return All entries. By default it returns only those on DNSBL.
 
    .PARAMETER DNSServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .EXAMPLE
    Find-DNSBL -IP '89.74.48.96' | Format-Table
 
    .EXAMPLE
    Find-DNSBL -IP '89.74.48.96', '89.74.48.97', '89.74.48.98' | Format-Table
 
    .EXAMPLE
    Find-DNSBL -IP '89.74.48.96' -DNSServer 1.1.1.1 | Format-Table
 
    .EXAMPLE
    Find-DNSBL -IP '89.74.48.96' -DNSProvider Cloudflare | Format-Table
 
    .NOTES
    General notes
    #>

    [alias('Find-BlackList', 'Find-BlockList')]
    [cmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Mandatory)][string[]] $IP,
        [string[]] $BlockListServers = $Script:BlockList,
        [switch] $All,
        [Parameter(ParameterSetName = 'DNSServer')][string] $DNSServer,
        [Parameter(ParameterSetName = 'DNSProvider')][ValidateSet('Cloudflare', 'Google')][string] $DNSProvider
    )
    foreach ($I in $IP) {
        foreach ($Server in $BlockListServers) {
            [string] $FQDN = $I -replace '^(\d+)\.(\d+)\.(\d+)\.(\d+)$', "`$4.`$3.`$2.`$1.$Server"
            if (-not $DNSProvider) {
                $DnsQuery = Resolve-DnsQuery -Name $FQDN -Type A -Server $DNSServer -All
                $Answer = $DnsQuery.Answers[0].Address.IPAddressToString
                $IsListed = $null -ne $Answer
            } else {
                $DnsQuery = Resolve-DnsQueryRest -Name $FQDN -Type A -DNSProvider $DNSProvider -All
                $Answer = $DnsQuery.Answers.Address
                $IsListed = $null -ne $DnsQuery.Answers
            }
            $Result = [PSCustomObject] @{
                IP         = $I
                FQDN       = $FQDN
                BlackList  = $Server
                IsListed   = $IsListed
                Answer     = $Answer
                TTL        = $DnsQuery.Answers.TimeToLive
                NameServer = $DnsQuery.NameServer
            }
            if (-not $All -and $Result.IsListed -eq $false) {
                continue
            }
            $Result
        }
    }
}

function Find-DNSSECRecord {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable
    )
    begin {
        $DNSSECAlgorithmNames = [ordered] @{
            '1'  = @{ Short = 'RSAMD5'; Long = 'RSA/MD5' }
            '2'  = @{ Short = 'DH'; Long = 'Diffie-Hellman' }
            '3'  = @{ Short = 'DSASHA1'; Long = 'DSA/SHA1' }
            '5'  = @{ Short = 'RSASHA1'; Long = 'RSA/SHA-1' }
            '6'  = @{ Short = 'DSANSEC3SHA1'; Long = 'DSA-NSEC3-SHA1' }
            '7'  = @{ Short = 'RSASHA1NSEC3SHA1'; Long = 'RSASHA1-NSEC3-SHA1' }
            '8'  = @{ Short = 'RSASHA256'; Long = 'RSA/SHA-256' }
            '10' = @{ Short = 'RSASHA512'; Long = 'RSA/SHA-512' }
            '12' = @{ Short = 'ECCGOST'; Long = 'GOST R 34.10-2001' }
            '13' = @{ Short = 'ECDSAP256SHA256'; Long = 'ECDSA Curve P-256 with SHA-256' }
            '14' = @{ Short = 'ECAP384SHA384'; Long = 'ECDSA Curve P-384 with SHA-384' }
            '15' = @{ Short = 'ED25519'; Long = 'Ed25519' }
            '16' = @{ Short = 'ED448'; Long = 'Ed448' }
        }
        $DNSSECAlgorithmNamesShortToLong = [ordered] @{
            'RSAMD5'           = 'RSA/MD5'
            'DH'               = 'Diffie-Hellman'
            'DSASHA1'          = 'DSA/SHA1'
            'RSASHA1'          = 'RSA/SHA-1'
            'DSANSEC3SHA1'     = 'DSA-NSEC3-SHA1'
            'RSASHA1NSEC3SHA1' = 'RSASHA1-NSEC3-SHA1'
            'RSASHA256'        = 'RSA/SHA-256'
            'RSASHA512'        = 'RSA/SHA-512'
            'ECCGOST'          = 'GOST R 34.10-2001'
            'ECDSAP256SHA256'  = 'ECDSA Curve P-256 with SHA-256'
            'ECAP384SHA384'    = 'ECDSA Curve P-384 with SHA-384'
            'ED25519'          = 'Ed25519'
            'ED448'            = 'Ed448'
        }
    }
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning 'Find-DNSSECRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "$D"
                Type        = 'DNSKEY'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                [Array] $DNSRecordAnswers = $DNSRecord.Answers

                if ($DNSProvider -eq 'Cloudflare') {
                    #Name Count TimeToLive Text
                    #---- ----- ---------- ----
                    #evotec.pl 48 3600 256 3 ECDSAP256SHA256 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
                    #evotec.pl 48 3600 257 3 ECDSAP256SHA256 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
                    foreach ($Record in $DNSRecordAnswers) {
                        $textParts = $Record.Text -split ' '
                        $algorithmNumber = $textParts[2]
                        $MailRecord = [ordered] @{
                            Name          = $D
                            Flags         = [int] $textParts[0]
                            Protocol      = [int] $textParts[1]
                            Algorithm     = $algorithmNumber
                            AlgorithmLong = $DNSSECAlgorithmNamesShortToLong[$algorithmNumber]
                            PublicKey     = $textParts[3..($textParts.Length - 1)] -join ' '
                            #PublicKeyAsString = $textParts[3..($textParts.Length - 1)] -join ' '
                            TimeToLive    = $Record.TimeToLive
                            QueryServer   = $DNSRecord.NameServer -join '; '
                            Advisory      = "DNSSEC record found."
                        }
                        if ($AsHashTable) {
                            $MailRecord
                        } else {
                            [PSCustomObject] $MailRecord
                        }
                    }
                } elseif ($DNSProvider -eq 'Google') {
                    #Name Count TimeToLive Text
                    #---- ----- ---------- ----
                    #evotec.pl. 48 3397 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
                    #evotec.pl. 48 3397 256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
                    # $DNSRecordAnswers
                    foreach ($Record in $DNSRecordAnswers) {
                        $textParts = $Record.Text -split ' '
                        $algorithmNumber = $textParts[2]
                        $algorithmNames = $DNSSECAlgorithmNames[$algorithmNumber]
                        $MailRecord = [ordered] @{
                            Name          = $D
                            Flags         = [int] $textParts[0]
                            Protocol      = [int] $textParts[1]
                            Algorithm     = $algorithmNames.Short
                            AlgorithmLong = $algorithmNames.Long
                            PublicKey     = $textParts[3..($textParts.Length - 1)] -join ' '
                            #PublicKeyAsString = $textParts[3..($textParts.Length - 1)] -join ' '
                            TimeToLive    = $Record.TimeToLive
                            QueryServer   = $DNSRecord.NameServer -join '; '
                            Advisory      = "DNSSEC record found."
                        }
                        if ($AsHashTable) {
                            $MailRecord
                        } else {
                            [PSCustomObject] $MailRecord
                        }
                    }
                } else {
                    foreach ($Record in $DNSRecordAnswers) {
                        $MailRecord = [ordered] @{
                            Name          = $D
                            Flags         = $Record.Flags             # : 257
                            Protocol      = $Record.Protocol          # : 3
                            Algorithm     = $Record.Algorithm         # : ECDSAP256SHA256
                            AlgorithmLong = $DNSSECAlgorithmNamesShortToLong[$Record.Algorithm.ToString()]
                            PublicKey     = $Record.PublicKeyAsString # : mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
                            TimeToLive    = $Record.TimeToLive
                            QueryServer   = $DNSRecord.NameServer
                            Advisory      = "DNSSEC record found."
                        }
                        if ($AsHashTable) {
                            $MailRecord
                        } else {
                            [PSCustomObject] $MailRecord
                        }
                    }
                }
                if ($DNSRecordAnswers.Count -eq 0) {
                    $MailRecord = [ordered] @{
                        Name          = $D
                        Flags         = ''
                        Protocol      = ''
                        Algorithm     = ''
                        AlgorithmLong = ''
                        PublicKey     = ''
                        TimeToLive    = ''
                        QueryServer   = $DNSRecord.NameServer
                        Advisory      = "No DNSSEC record found."
                    }
                    if ($AsHashTable) {
                        $MailRecord
                    } else {
                        [PSCustomObject] $MailRecord
                    }
                }
            } catch {
                $MailRecord = [ordered] @{
                    Name          = $D
                    Flags         = ''
                    Protocol      = ''
                    Algorithm     = ''
                    AlgorithmLong = ''
                    PublicKey     = ''
                    TimeToLive    = ''
                    QueryServer   = if ($DNSRecord.NameServer) {
                        $DNSRecord.NameServer 
                    } elseif ($DNSProvider) {
                        $DNSProvider 
                    } else {
                        $DnsServer 
                    }
                    Advisory      = "No DNSSEC record found."
                }
                if ($AsHashTable) {
                    $MailRecord
                } else {
                    [PSCustomObject] $MailRecord
                }
                Write-Warning "Find-DNSSECRecord - $_"
            }
        }
    }
}
function Find-IPGeolocation {
    <#
    .SYNOPSIS
    Get IP Geolocation information from ip-api.com
 
    .DESCRIPTION
    Get IP Geolocation information from ip-api.com
    It uses ip-api.com free API to get geolocation information.
    This API is limited to 45 requests per minute from an IP address.
 
    .PARAMETER IPAddress
    Parameter description
 
    .EXAMPLE
    Find-IPGeolocation -IPAddress '1.1.1.1'
 
    .EXAMPLE
    Find-IPGeolocation -IPAddress '1.1.1.1', '1.1.1.2'
 
    .NOTES
    Due to free API usage it's not possible to query API using HTTPS.
 
    #>

    [alias('Get-IPGeolocation')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]] $IPAddress,
        [ValidateSet(
            'status', 'message', 'continent', 'continentCode', 'country', 'countryCode', 'region', 'regionName', 'city', 'district',
            'zip', 'lat', 'lon', 'timezone', 'offset', 'currency', 'isp', 'org', 'as', 'asname', 'reverse', 'mobile', 'proxy', 'hosting', 'query'
        )][string[]] $Fields,
        [switch] $Force
    )

    if (-not $Script:CachedGEO -or $Force.IsPresent) {
        $Script:CachedGEO = [ordered] @{}
    }
    foreach ($IP in $IPAddress) {
        $QueryParameters = [ordered] @{
            fields = $Fields
        }
        Remove-EmptyValue -Hashtable $QueryParameters
        $Uri = Join-UriQuery -BaseUri 'http://ip-api.com/json' -QueryParameter $QueryParameters -RelativeOrAbsoluteUri $IP

        #$Result = Invoke-RestMethod -Uri $Uri -Method Get -ErrorAction Stop
        if (-not $Script:CachedGEO[$IP]) {
            Write-Verbose -Message "Find-IPGeolocation - Querying API for $IP"
            if ($Script:GeoHeaders -and $Script:GeoHeaders.Date -and $Script:GeoHeaders.Date.AddSeconds(60) -gt [DateTime]::Now) {
                if ($Script:GeoHeaders.Headers.'X-Rl' -le 0) {
                    $TimeToSleep = $Script:GeoHeaders.Headers.'X-Ttl' + 1
                    Write-Warning -Message "Get-IPGeolocation - API limit reached. Waiting $TimeToSleep seconds."
                    Start-Sleep -Seconds $TimeToSleep
                }
            }
            try {
                $Result = Invoke-WebRequest -Uri $Uri -Method Get -ErrorAction Stop -UseBasicParsing -Verbose:$false
                $Script:GeoHeaders = [ordered] @{
                    Date    = [DateTime]::Now
                    Headers = $Result.Headers
                }
                $ResultJSON = $Result.Content | ConvertFrom-Json -ErrorAction Stop
                if ($ResultJSON) {
                    $Script:CachedGEO[$IP] = $ResultJSON
                    $ResultJSON
                }
            } catch {
                Write-Warning -Message "Find-IPGeolocation - Couldn't query API for $IP. Error: $($_.Exception.Message)"
            }
        } else {
            Write-Verbose -Message "Find-IPGeolocation - Using cached data for $IP"
            $Script:CachedGEO[$IP]
        }
    }
}
function Find-MTASTSRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide MTA-STS information, and verifies if configuration exists
 
    .DESCRIPTION
    Queries DNS to provide MTA-STS information, and verifies if configuration exists
 
    .PARAMETER DomainName
    Name/DomainName to query for MTA-STS record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    Find-MTASTSRecord -DomainName 'google.com' -DNSProvider Google | Format-Table
 
    .EXAMPLE
    Find-MTASTSRecord -DomainName 'google.com' -DNSProvider Cloudflare | Format-Table
 
    .EXAMPLE
    Find-MTASTSRecord -DomainName 'google.com' | Format-Table
 
    .EXAMPLE
    Find-MTASTSRecord -DomainName 'evotec.xyz' | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning -Message 'Find-MTASTSRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "_mta-sts.$D"
                Type        = 'TXT'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                [Array] $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'v=STSv1;'

                $Version = $null
                $MxRecords = [System.Collections.Generic.List[string]]::new()
                $Mode = $null
                $MaxAge = $null

                if ($DNSRecordAnswers.Count -eq 1) {
                    $Url = "https://mta-sts.$D/.well-known/mta-sts.txt"
                    try {
                        $Response = Invoke-RestMethod -Uri $Url -ErrorAction Stop
                        $ResponseData = $Response.Trim() -split "`r`n"
                        foreach ($Data in $ResponseData) {
                            if ($Data.StartsWith("version: ")) {
                                $Version = $Data.Substring(9)
                            } elseif ($Data.StartsWith("mode: ")) {
                                $Mode = $Data.Substring(6)
                            } elseif ($Data.StartsWith("mx: ")) {
                                $MxRecords.Add($Data.Substring(4))
                            } elseif ($Data.StartsWith("max_age: ")) {
                                $MaxAge = $Data.Substring(9)
                            }
                        }
                    } catch {
                        Write-Warning -Message "Find-MTASTSRecord - Error when getting data from $($Url): $_"
                    }
                } elseif ($DNSRecordAnswers.Count -gt 1) {
                    Write-Warning -Message "Find-MTASTSRecord - More than one MTA-STS record found for $D, verification skipped"
                }
                if (-not $AsObject) {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        MTASTS      = $DNSRecordAnswers.Text -join '; '
                        Version     = $Version
                        Mode        = $Mode
                        MxRecords   = $MxRecords
                        MaxAge      = $MaxAge
                        TimeToLive  = $DNSRecordAnswers.TimeToLive -join '; '

                        QueryServer = $DNSRecord.NameServer -join '; '
                    }
                } else {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        MTASTS      = $DNSRecordAnswers.Text
                        Version     = $Version
                        Mode        = $Mode
                        MxRecords   = $MxRecords
                        MaxAge      = $MaxAge
                        TimeToLive  = $DNSRecordAnswers.TimeToLive
                        QueryServer = $DNSRecord.NameServer
                    }
                }
            } catch {
                $MailRecord = [ordered] @{
                    Name        = $D
                    MTASTS      = ""
                    Version     = ""
                    Mode        = ""
                    MxRecords   = ""
                    MaxAge      = ""
                    TimeToLive  = ''
                    QueryServer = ''
                }
                Write-Warning -Message "Find-MTASTSRecord - Error: $($_.Exception.Message)"
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}
function Find-MxRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide MX information
 
    .DESCRIPTION
    Queries DNS to provide MX information
 
    .PARAMETER DomainName
    Name/DomainName to query for MX record
 
    .PARAMETER ResolvePTR
    Parameter description
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .PARAMETER Separate
    Returns each MX record separatly
 
    .EXAMPLE
    # Standard way
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' | Format-Table *
 
    .EXAMPLE
    # Https way via Cloudflare
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Cloudflare | Format-Table *
 
    .EXAMPLE
    # Https way via Google
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Google | Format-Table *
 
    .EXAMPLE
    # Standard way with ResolvePTR
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' -ResolvePTR | Format-Table *
 
    .EXAMPLE
    # Https way via Cloudflare with ResolvePTR
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Cloudflare -ResolvePTR | Format-Table *
 
    .EXAMPLE
    # Https way via Google with ResolvePTR
    Find-MxRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Google -ResolvePTR | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array]$DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $ResolvePTR,
        [switch] $AsHashTable,
        [switch] $Separate,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning 'Find-MxRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = $D
                Type        = 'MX'
                ErrorAction = 'SilentlyContinue'
            }
            if ($DNSProvider) {
                $MX = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
            } else {
                if ($DnsServer) {
                    $Splat['Server'] = $DnsServer
                }
                $MX = Resolve-DnsQuery @Splat -All
            }
            [Array] $MXRecords = foreach ($MXRecord in $MX.Answers) {
                $MailRecord = [ordered] @{
                    Name        = $D
                    Preference  = $MXRecord.Preference
                    TimeToLive  = $MXRecord.TimeToLive
                    MX          = ($MXRecord.Exchange) -replace '.$'
                    QueryServer = $MX.NameServer
                }
                [Array] $IPAddresses = foreach ($Record in $MX.Answers.Exchange) {
                    $Splat = @{
                        Name        = $Record
                        Type        = 'A'
                        ErrorAction = 'SilentlyContinue'
                    }
                    if ($DNSProvider) {
                        (Resolve-DnsQueryRest @Splat -DNSProvider $DnsProvider) | ForEach-Object { $_.Address }
                    } else {
                        if ($DnsServer) {
                            $Splat['Server'] = $DnsServer
                        }
                        (Resolve-DnsQuery @Splat) | ForEach-Object { $_.Address.IPAddressToString }
                    }
                }
                $MailRecord['IPAddress'] = $IPAddresses
                if ($ResolvePTR) {
                    $MailRecord['PTR'] = foreach ($IP in $IPAddresses) {
                        $Splat = @{
                            Name        = $IP
                            Type        = 'PTR'
                            ErrorAction = 'SilentlyContinue'
                        }
                        if ($DNSProvider) {
                            (Resolve-DnsQueryRest @Splat -DNSProvider $DnsProvider) | ForEach-Object { $_.Text -replace '.$' }
                        } else {
                            if ($DnsServer) {
                                $Splat['Server'] = $DnsServer
                            }
                            (Resolve-DnsQuery @Splat) | ForEach-Object { $_.PtrDomainName -replace '.$' }
                        }
                    }
                }
                $MailRecord
            }
            if ($Separate) {
                foreach ($MXRecord in $MXRecords) {
                    if ($AsHashTable) {
                        $MXRecord
                    } else {
                        [PSCustomObject] $MXRecord
                    }
                }
            } else {
                if (-not $AsObject) {
                    $MXRecord = [ordered] @{
                        Name        = $D
                        Count       = $MXRecords.Count
                        Preference  = $MXRecords.Preference -join '; '
                        TimeToLive  = $MXRecords.TimeToLive -join '; '
                        MX          = $MXRecords.MX -join '; '
                        IPAddress   = ($MXRecords.IPAddress | Sort-Object -Unique) -join '; '
                        QueryServer = $MXRecords.QueryServer -join '; '
                    }
                    if ($ResolvePTR) {
                        $MXRecord['PTR'] = ($MXRecords.PTR | Sort-Object -Unique) -join '; '
                    }
                } else {
                    $MXRecord = [ordered] @{
                        Name        = $D
                        Count       = $MXRecords.Count
                        Preference  = $MXRecords.Preference
                        TimeToLive  = $MXRecords.TimeToLive
                        MX          = $MXRecords.MX
                        IPAddress   = ($MXRecords.IPAddress | Sort-Object -Unique)
                        QueryServer = $MXRecords.QueryServer
                    }
                    if ($ResolvePTR) {
                        $MXRecord['PTR'] = ($MXRecords.PTR | Sort-Object -Unique)
                    }
                }
                if ($AsHashTable) {
                    $MXRecord
                } else {
                    [PSCustomObject] $MXRecord
                }
            }
        }
    }
}
function Find-O365OpenIDRecord {
    <#
    .SYNOPSIS
    Quick way to find Office 365 Tenant ID by using domain name
 
    .DESCRIPTION
    Quick way to find Office 365 Tenant ID by using domain name
 
    .PARAMETER Domain
    Domain name to check
 
    .EXAMPLE
    Find-O365OpenIDRecord -Domain 'evotec.pl'
 
    .NOTES
    General notes
    #>

    [alias('Find-O365TenantID')]
    [cmdletbinding()]
    param(
        [parameter(Mandatory)][alias('DomainName')][string] $Domain
    )
    $Invoke = Invoke-RestMethod "https://login.windows.net/$Domain/.well-known/openid-configuration" -Method GET -Verbose:$false
    if ($Invoke) {
        $Invoke.userinfo_endpoint.Split("/")[3]
    }
}
function Find-SecurityTxtRecord {
    <#
    .SYNOPSIS
    Queries website to provide security.txt information
 
    .DESCRIPTION
    Queries website to provide security.txt information
 
    .PARAMETER DomainName
    Domain Name to query for security.txt record
 
    .EXAMPLE
    Find-SecurityTxtRecord -DomainName 'evotec.xyz', 'evotec.pl', 'google.com', 'facebook.com', 'www.gemini.com' | Format-Table -AutoSize *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName
    )

    process {
        foreach ($Domain in $DomainName) {
            $Response = $null
            $DataFound = $false
            $DataOutput = [ordered]@{
                Domain                 = $Domain
                RecordPresent          = $false
                PGPSigned              = $false # but not verified, we would need to use PSGPG module for that
                FallbackUsed           = $false
                Url                    = $null
                ContactEmail           = [System.Collections.Generic.List[string]]::new()
                ContactWebsite         = [System.Collections.Generic.List[string]]::new()
                ContactOther           = [System.Collections.Generic.List[string]]::new()
                Acknowledgments        = [System.Collections.Generic.List[string]]::new()
                'Preferred-Languages'  = [System.Collections.Generic.List[string]]::new()
                Canonical              = [System.Collections.Generic.List[string]]::new()
                Expires                = [System.Collections.Generic.List[string]]::new()
                Encryption             = [System.Collections.Generic.List[string]]::new()
                Policy                 = [System.Collections.Generic.List[string]]::new()
                Hiring                 = [System.Collections.Generic.List[string]]::new()
                'Signature-Encryption' = [System.Collections.Generic.List[string]]::new()
            }

            $Url = "https://$Domain/.well-known/security.txt"
            try {
                $Response = Invoke-RestMethod -Uri $Url -ErrorAction Stop
                $DataFound = $true
            } catch {
                Write-Warning -Message "Find-SecurityTxtRecord - Error when getting data from $($Url): $($_.Exception.message)"
            }
            if (-not $DataFound) {
                $Url = "http://$Domain/security.txt"
                try {
                    $Response = Invoke-RestMethod -Uri $Url -ErrorAction Stop
                    $DataOutput['FallbackUsed'] = $true
                    $DataFound = $true
                } catch {
                    Write-Warning -Message "Find-SecurityTxtRecord - Error when getting data from $($Url): $($_.Exception.message)"
                }
            }
            if ($DataFound) {
                $DataOutput['Url'] = $Url
                $ResponseData = $Response.Trim() -split "`r`n" -split "`n"
                $DataOutput['RecordPresent'] = $true
                foreach ($Data in $ResponseData) {
                    if ($Data.StartsWith("Contact:")) {
                        $Value = $Data.Replace("Contact:", "").Trim()
                        if ($Value -match "mailto:") {
                            $Value = $Value.Replace("mailto:", "")
                            $DataOutput['ContactEmail'].Add($Value)
                        } elseif ($Value -like "*://*") {
                            $DataOutput['ContactWebsite'].Add($Value)
                        } elseif ($Value -like "*@*") {
                            $DataOutput['ContactEmail'].Add($Value)
                        } else {
                            $DataOutput['ContactWebsite'].Add($Value)
                        }
                    } elseif ($Data.StartsWith("Acknowledgments:")) {
                        $DataOutput['Acknowledgments'].Add($Data.Replace("Acknowledgments:", "").Trim())
                    } elseif ($Data.StartsWith("Preferred-Languages:")) {
                        $DataOutput['Preferred-Languages'].Add($Data.Replace("Preferred-Languages:", "").Trim())
                    } elseif ($Data.StartsWith("Canonical:")) {
                        $DataOutput['Canonical'].Add($Data.Replace("Canonical:", "").Trim())
                    } elseif ($Data.StartsWith("Expires:")) {
                        $DataOutput['Expires'].Add($Data.Replace("Expires:", "").Trim())
                    } elseif ($Data.StartsWith("Encryption:")) {
                        $DataOutput['Encryption'].Add($Data.Replace("Encryption:", "").Trim())
                    } elseif ($Data.StartsWith("Policy:")) {
                        $DataOutput['Policy'].Add($Data.Replace("Policy:", "").Trim())
                    } elseif ($Data.StartsWith("Hiring:")) {
                        $DataOutput['Hiring'].Add($Data.Replace("Hiring:", "").Trim())
                    } elseif ($Data.StartsWith("Signature-Encryption:")) {
                        $DataOutput['Signature-Encryption'].Add($Data.Replace("Signature-Encryption:", "").Trim())
                    } elseif ($Data.StartsWith("-----BEGIN PGP SIGNED MESSAGE-----")) {
                        $DataOutput['PGPSigned'] = $true
                    } else {
                        # this won't really work, as some companies like gemini add full blown text
                        # https://www.gemini.com/.well-known/security.txt
                        # $DataField = $Data.Split(":")[0]
                        # $DataValue = $Data.Split(":")[1].Trim()
                        # if (-not $DataOutput[$DataField]) {
                        # $DataOutput[$DataField] = [System.Collections.Generic.List[string]]::new()
                        # }
                        # $DataOutput[$DataField].Add($DataValue)
                    }
                }

                # Lets fix the output, to not have arrays of 1 element
                foreach ( $Key in [string[]] $DataOutput.Keys) {
                    $DataOutput[$Key] = if ($DataOutput[$Key] -is [Array] -and $DataOutput[$Key].Count -eq 1) {
                        $DataOutput[$Key][0]
                    } else {
                        $DataOutput[$Key]
                    }
                }
            }
            [PSCustomObject] $DataOutput
        }
    }
}
function Find-SPFRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide SPF information
 
    .DESCRIPTION
    Queries DNS to provide SPF information
 
    .PARAMETER DomainName
    Name/DomainName to query for SPF record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    # Standard way
    Find-SPFRecord -DomainName 'evotec.pl', 'evotec.xyz' | Format-Table *
 
    .EXAMPLE
    # Https way via Cloudflare
    Find-SPFRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Cloudflare | Format-Table *
 
    .EXAMPLE
    # Https way via Google
    Find-SPFRecord -DomainName 'evotec.pl', 'evotec.xyz' -DNSProvider Google | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array]$DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    begin {
        $SPFTags = [ordered] @{
            'v'        = 'SPFVersion'
            'all'      = 'All'
            #'a' = 'A'
            #'mx' = 'MX'
            #'ptr' = 'PTR'
            #'ip4' = 'IP4'
            #'ip6' = 'IP6'
            #'include' = 'Include'
            #'exists' = 'Exists'
            'redirect' = 'Redirect'
            'exp'      = 'Exp'
        }
    }
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning 'Find-MxRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = $D
                Type        = 'txt'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'spf1'
                if ($DNSRecordAnswers -is [array]) {
                    $Count = $DNSRecordAnswers.Count
                } elseif ($DNSRecordAnswers) {
                    $Count = 1
                } else {
                    $Count = 0
                }
                if (-not $AsObject) {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        #Count = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive -join '; '
                        SPF         = $DNSRecordAnswers.Text -join '; '
                        QueryServer = $DNSRecord.NameServer
                        Advisory    = "No SPF record found."
                    }
                } else {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        #Count = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive
                        SPF         = $DNSRecordAnswers.Text
                        QueryServer = $DNSRecord.NameServer
                        Advisory    = "No SPF record found."
                    }
                }
                # Split the SPF record into an array of settings
                $SPFSettings = $MailRecord.SPF -split ' '
                # Initialize an empty hashtable to store the settings
                $SPFSettingsHashTable = [ordered] @{}
                # Loop through each setting
                foreach ($setting in $SPFSettings) {
                    # Check if the setting is the SPF version
                    if ($setting -match '^v=') {
                        $Key = 'v'
                        $Value = $setting -replace 'v=', ''
                    } elseif ($setting -match '^[+~-]?all$') {
                        # Check if the setting is the 'all' mechanism
                        $Key = 'all'
                        switch ($setting) {
                            '+all' {
                                $Value = 'Pass' 
                            }
                            '-all' {
                                $Value = 'Fail' 
                            }
                            '~all' {
                                $Value = 'SoftFail' 
                            }
                            '?all' {
                                $Value = 'Neutral' 
                            }
                            default {
                                $Value = 'Neutral' 
                            }
                        }
                    } elseif ($setting -match ':') {
                        # Check if the setting contains a colon
                        # Split the setting into a key-value pair
                        $KeyValuePair = $setting -split ':'
                        $Key = $KeyValuePair[0].Trim()
                        $Value = $KeyValuePair[1]
                    } else {
                        # If the setting does not contain a colon, the whole setting is the key
                        $Key = $setting.Trim()
                        $Value = $true
                    }
                    # Add the key-value pair to the hashtable
                    $SPFSettingsHashTable[$Key] = $Value
                }
                foreach ($Tag in $SPFTags.Keys) {
                    # If the tag is in the SPF record, add its value to the MailRecord
                    if ($SPFSettingsHashTable[$Tag]) {
                        $MailRecord[$SPFTags[$Tag]] = $SPFSettingsHashTable[$Tag]
                    } else {
                        $MailRecord[$SPFTags[$tag]] = $null
                    }
                }
                # Lets do some advisory logic
                if ($Count -eq 0) {
                    $MailRecord['Advisory'] = "No SPF record found."
                } elseif ($Count -gt 1) {
                    $MailRecord['Advisory'] = "Multiple SPF records found. This is not allowed (as per RFC4408)"
                } else {
                    switch -Regex ($MailRecord.SPF) {
                        '-all' {
                            $SpfAdvisory = "An SPF record is configured and the policy is strict."
                        }
                        '~all' {
                            $SpfAdvisory = "An SPF record is configured but the policy is not strict."
                        }
                        "\?all" {
                            $SpfAdvisory = "An SPF record is configured but the policy is not effective."
                        }
                        '\+all' {
                            $SpfAdvisory = "An SPF record is configured but the policy is not effective."
                        }
                        Default {
                            $SpfAdvisory = "No qualifier found. Policy is not effective."
                        }
                    }
                    if ($MailRecord.SPF.Length -gt 255) {
                        $SpfAdvisory = "SPF record is too long. It's recommended to keep it under 255 characters (as per RFC4408)"
                    } else {
                        $MailRecord['Advisory'] = $SpfAdvisory
                    }
                }

                # Check if there are more than 10 DNS lookups in the SPF record
                $dnsLookupMechanisms = 'a', 'mx', 'ptr', 'exists', 'include', 'redirect'
                $dnsLookupCount = ($SPFSettings | Where-Object { $_ -in $dnsLookupMechanisms }).Count
                if ($dnsLookupCount -gt 10) {
                    $SpfAdvisory = "Invalid SPF record - SPF record requires more than 10 DNS lookups"
                }
                # Split each setting into its mechanism/modifier name and value
                $SPFSettingNames = $SPFSettings | ForEach-Object {
                    if ($_ -match '[:=]') {
                        $settingName = $_ -split '[:=]' | Select-Object -First 1
                    } else {
                        $settingName = $_
                    }
                    return $settingName
                }
                # Check if there are any unrecognized mechanisms or modifiers
                $validMechanismsAndModifiers = 'v', 'a', 'mx', 'ptr', 'ip4', 'ip6', 'include', 'exists', 'redirect', 'exp', '-all', '~all', '?all', '+all'
                $invalidMechanismsAndModifiers = $SPFSettingNames | Where-Object { $_ -notin $validMechanismsAndModifiers }
                if ($invalidMechanismsAndModifiers) {
                    $SpfAdvisory = "Invalid SPF record - SPF record contains unrecognized mechanisms or modifiers: $($invalidMechanismsAndModifiers -join ', ')"
                }
                # Check if the SPF record starts with 'v=spf1'
                if ($SPFSettings[0] -ne 'v=spf1') {
                    $SpfAdvisory = "Invalid SPF record - SPF record does not start with 'v=spf1'"
                }
                # Check if there is at least one mechanism present
                $mechanisms = 'all', 'a', 'mx', 'ptr', 'ip4', 'ip6', 'include', 'exists', 'redirect', 'exp'
                if (-not ($SPFSettingsHashTable.Keys | Where-Object { $_ -in $mechanisms })) {
                    $SpfAdvisory = "Invalid SPF record - no mechanisms present"
                }
                # Check if the 'all' mechanism, if present, is the last mechanism in the record
                $allMechanism = $SPFSettings | Where-Object { $_ -match '^[+~-]?all$' }
                if ($allMechanism -and $SPFSettings[-1] -notmatch '^[+~-]?all$') {
                    $SpfAdvisory = "Invalid SPF record - 'all' mechanism is not the last mechanism"
                }
                $MailRecord['Advisory'] = $SpfAdvisory
            } catch {
                $MailRecord = [ordered] @{
                    Name        = $D
                    #Count = 0
                    TimeToLive  = ''
                    SPF         = ''
                    QueryServer = ''
                    Advisory    = "No SPF record found."
                }
                Write-Warning "Find-SPFRecord - $_"
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}
function Find-TLSRPTRecord {
    <#
    .SYNOPSIS
    Queries DNS to provide TLS-RPT information
 
    .DESCRIPTION
    Queries DNS to provide TLS-RPT information
 
    .PARAMETER DomainName
    Name/DomainName to query for DMARC record
 
    .PARAMETER DnsServer
    Allows to choose DNS IP address to ask for DNS query. By default uses system ones.
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google)
 
    .PARAMETER AsHashTable
    Returns Hashtable instead of PSCustomObject
 
    .PARAMETER AsObject
    Returns an object rather than string based represantation for name servers (for easier display purposes)
 
    .EXAMPLE
    Find-TLSRPTRecord -DomainName 'evotec.xyz' -DNSProvider Cloudflare
 
    .EXAMPLE
    Find-TLSRPTRecord -DomainName 'evotec.xyz' -DNSProvider
 
    .EXAMPLE
    Find-TLSRPTRecord -DomainName 'evotec.xyz'
 
    .NOTES
    SMTP TLS Reporting (TLS-RPT) is a standard that enables the reporting of issues in TLS connectivity
    that is experienced by applications that send emails and detect misconfigurations.
    It enables the reporting of email delivery issues that take place when an email isn't encrypted with TLS.
    In September 2018 the standard was first documented in RFC 8460.
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Array] $DomainName,
        [string] $DnsServer,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider,
        [switch] $AsHashTable,
        [switch] $AsObject
    )
    process {
        foreach ($Domain in $DomainName) {
            if ($Domain -is [string]) {
                $D = $Domain
            } elseif ($Domain -is [System.Collections.IDictionary]) {
                $D = $Domain.DomainName
                if (-not $D) {
                    Write-Warning 'Find-TLSRPTRecord - property DomainName is required when passing Array of Hashtables'
                }
            }
            $Splat = @{
                Name        = "_smtp._tls.$D"
                Type        = 'TXT'
                ErrorAction = 'Stop'
            }
            try {
                if ($DNSProvider) {
                    $DNSRecord = Resolve-DnsQueryRest @Splat -All -DNSProvider $DnsProvider
                } else {
                    if ($DnsServer) {
                        $Splat['Server'] = $DnsServer
                    }
                    $DNSRecord = Resolve-DnsQuery @Splat -All
                }
                [Array] $DNSRecordAnswers = $DNSRecord.Answers | Where-Object Text -Match 'TLSRPTv1'
                if (-not $AsObject) {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive -join '; '
                        TLSRPT      = $DNSRecordAnswers.Text -join '; '
                        QueryServer = $DNSRecord.NameServer -join '; '
                    }
                } else {
                    $MailRecord = [ordered] @{
                        Name        = $D
                        Count       = $DNSRecordAnswers.Count
                        TimeToLive  = $DNSRecordAnswers.TimeToLive
                        TLSRPT      = $DNSRecordAnswers.Text
                        QueryServer = $DNSRecord.NameServer
                    }
                }
            } catch {
                $MailRecord = [ordered] @{
                    Name        = $D
                    Count       = 0
                    TimeToLive  = ''
                    TLSRPT      = ''
                    QueryServer = ''
                }
                Write-Warning "Find-TLSRPTRecord - $_"
            }
            if ($AsHashTable) {
                $MailRecord
            } else {
                [PSCustomObject] $MailRecord
            }
        }
    }
}


function Get-DMARCData {
    <#
    .SYNOPSIS
    Read DMARC report file and return data in PowerShell object
 
    .DESCRIPTION
    Read DMARC report file and return data in PowerShell object
 
    .PARAMETER Path
    Path to the DMARC report file
 
    .EXAMPLE
    Get-DMARCData -Path 'C:\Temp\DMARC\report.xml'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory)][alias('FilePath')][string] $Path
    )
    $Year1970 = Get-Date -Year 1970 -Month 1 -Day 1 00:00:00
    if ($Path -and (Test-Path -LiteralPath $Path)) {
        try {
            [xml]$Report = Get-Content -Path $Path -Raw -ErrorAction Stop
        } catch {
            Write-Warning "Get-DMARCData - Couldn't read file $Path. Error: $($_.Exception.Message)"
            return
        }
        if ($Report.feedback) {
            $DMARCReport = $Report.feedback

            $DateBegin = $Year1970 + ([System.TimeSpan]::fromseconds($DMARCReport.report_metadata.Date_Range.Begin))
            $DateEnd = $Year1970 + ([System.TimeSpan]::fromseconds($DMARCReport.report_metadata.Date_Range.End))

            $OutputData = [ordered] @{
                MetaData        = [ordered] @{
                    OrgName          = $DMARCReport.report_metadata.Org_Name
                    Email            = $DMARCReport.report_metadata.Email
                    ExtraContactInfo = $DMARCReport.report_metadata.Extra_Contact_Info
                    ReportID         = $DMARCReport.report_metadata.Report_ID
                    DateRangeBegin   = $DateBegin # 1580342400
                    DateRangeEnd     = $DateEnd # 1580428799
                }
                PolicyPublished = [ordered] @{
                    Domain = $DMARCReport.policy_published.Domain
                    ADKIM  = $DMARCReport.policy_published.ADKIM
                    ASPF   = $DMARCReport.policy_published.ASPF
                    P      = $DMARCReport.policy_published.P
                    SP     = $DMARCReport.policy_published.SP
                    PCT    = $DMARCReport.policy_published.PCT
                }
                Records         = $null
            }
            $OutputData['Records'] = foreach ($Record in $DMARCReport.record) {
                $Object = [ordered] @{
                    HeaderFrom  = $Record.identifiers.header_from
                    SourceIP    = $Record.Row.source_ip
                    Date        = $DateBegin
                    Count       = $Record.Row.count
                    Disposition = $Record.Row.policy_evaluated.disposition
                    DKIM        = $Record.Row.policy_evaluated.dkim
                    SPF         = $Record.Row.policy_evaluated.spf
                }
                $Count = 0
                foreach ($Dkim in $Record.auth_results.dkim) {
                    $Count++
                    $Object["AuthResultsDKIMDomain$Count"] = $Dkim.domain
                    $Object["AuthResultsDKIMResult$Count"] = $Dkim.result
                    $Object["AuthResultsDKIMSelector$Count"] = $Dkim.selector
                }
                $Count = 0
                foreach ($SPF in $Record.auth_results.spf) {
                    $Count++
                    $Object["AuthResultsSPFDomain$Count"] = $Record.auth_results.spf.domain
                    $Object["AuthResultsSPFResult$Count"] = $Record.auth_results.spf.result
                }
                [PSCustomObject]$Object
            }
        }
        $OutputData
    }
}
function Get-IMAPFolder {
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $Client,
        [MailKit.FolderAccess] $FolderAccess = [MailKit.FolderAccess]::ReadOnly
    )
    if ($Client) {
        $Folder = $Client.Data.Inbox
        $null = $Folder.Open($FolderAccess)

        Write-Verbose "Get-IMAPMessage - Total messages $($Folder.Count), Recent messages $($Folder.Recent)"
        $Client.Messages = $Folder
        $Client.Count = $Folder.Count
        $Client.Recent = $Folder.Recent
        $Client
    } else {
        Write-Verbose 'Get-IMAPMessage - Client not connected?'
    }
}
function Get-IMAPMessage {
    [cmdletBinding()]
    param(
        [Parameter()][System.Collections.IDictionary] $Client,
        [MailKit.FolderAccess] $FolderAccess = [MailKit.FolderAccess]::ReadOnly
    )
    if ($Client) {
        $Folder = $Client.Data.Inbox
        $null = $Folder.Open($FolderAccess)

        Write-Verbose "Get-IMAPMessage - Total messages $($Folder.Count), Recent messages $($Folder.Recent)"
        $Client.Folder = $Folder
    } else {
        Write-Verbose 'Get-IMAPMessage - Client not connected?'
    }
}
function Get-MailFolder {
    [cmdletBinding()]
    param(
        [string] $UserPrincipalName,
        [PSCredential] $Credential
    )
    if ($Credential) {
        $AuthorizationData = ConvertFrom-GraphCredential -Credential $Credential
    } else {
        return
    }
    if ($AuthorizationData.ClientID -eq 'MSAL') {
        $Authorization = Connect-O365GraphMSAL -ApplicationKey $AuthorizationData.ClientSecret
    } else {
        $Authorization = Connect-O365Graph -ApplicationID $AuthorizationData.ClientID -ApplicationKey $AuthorizationData.ClientSecret -TenantDomain $AuthorizationData.DirectoryID -Resource https://graph.microsoft.com
    }
    Invoke-O365Graph -Headers $Authorization -Uri "/users/$UserPrincipalName/mailFolders" -Method GET
}
function Get-MailMessage {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER UserPrincipalName
    UserPrincipalName of the mailbox to get mails from
 
    .PARAMETER Credential
    Credential parameter is used to securely pass tokens/api keys for Graph API
 
    .PARAMETER All
    Parameter description
 
    .PARAMETER Limit
    Parameter description
 
    .PARAMETER Property
    Property parameter is used to select which properties to return.
    You can use any of the following properties:
    'createdDateTime', 'lastModifiedDateTime', 'changeKey', 'categories', 'receivedDateTime',
    'sentDateTime', 'hasAttachments', 'internetMessageId', 'subject', 'bodyPreview', 'importance',
    'parentFolderId', 'conversationId', 'conversationIndex', 'isDeliveryReceiptRequested',
    'isReadReceiptRequested', 'isRead', 'isDraft', 'webLink', 'inferenceClassification',
    'body', 'sender', 'from', 'toRecipients', 'ccRecipients', 'bccRecipients', 'replyTo', 'flag'
 
    You can also leave it empty to get default properties as returned by Graph API
 
    .PARAMETER Filter
    Filter parameter is used to filter results.
 
    .PARAMETER MgGraphRequest
    Forces using Invoke-MgGraphRequest internally.
    This allows to use Connect-MgGraph to authenticate and then use Get-MailMessage without any additional parameters.
 
    .EXAMPLE
    An example
 
    .NOTES
    Filtering using Graph API is quite complicated. You can find more information here:
    - https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http
 
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $UserPrincipalName,
        [PSCredential] $Credential,
        [switch] $All,
        [int] $Limit = 10,
        [ValidateSet(
            'createdDateTime', 'lastModifiedDateTime', 'changeKey', 'categories', 'receivedDateTime',
            'sentDateTime', 'hasAttachments', 'internetMessageId', 'subject', 'bodyPreview', 'importance',
            'parentFolderId', 'conversationId', 'conversationIndex', 'isDeliveryReceiptRequested',
            'isReadReceiptRequested', 'isRead', 'isDraft', 'webLink', 'inferenceClassification',
            'body', 'sender', 'from', 'toRecipients', 'ccRecipients', 'bccRecipients', 'replyTo', 'flag'
        )
        ][string[]] $Property,
        [string] $Filter,
        [switch] $MgGraphRequest
    )
    if ($MgGraphRequest) {
        # do nothing, as we're using Connect-MgGraph
    } else {
        if ($Credential) {
            $AuthorizationData = ConvertFrom-GraphCredential -Credential $Credential
        } else {
            return
        }
        if ($AuthorizationData.ClientID -eq 'MSAL') {
            $Authorization = Connect-O365GraphMSAL -ApplicationKey $AuthorizationData.ClientSecret
        } else {
            $Authorization = Connect-O365Graph -ApplicationID $AuthorizationData.ClientID -ApplicationKey $AuthorizationData.ClientSecret -TenantDomain $AuthorizationData.DirectoryID -Resource https://graph.microsoft.com
        }
    }

    $QueryParameters = [ordered] @{
        filter = $Filter
        select = $Property -join ','
    }
    $joinUriQuerySplat = @{
        BaseUri               = 'https://graph.microsoft.com/v1.0'
        QueryParameter        = $QueryParameters
        RelativeOrAbsoluteUri = "/users/$UserPrincipalName/messages"
    }
    Remove-EmptyValue -Hashtable $joinUriQuerySplat -Recursive -Rerun 2

    $Uri = Join-UriQuery @joinUriQuerySplat

    Write-Verbose "Get-MailMessage - Executing $Uri"
    if ($All) {
        Invoke-O365Graph -Headers $Authorization -Uri $Uri -Method GET -MGGraphRequest:$MgGraphRequest.IsPresent -FullUri
    } else {
        Invoke-O365Graph -Headers $Authorization -Uri $Uri -Method GET -MGGraphRequest:$MgGraphRequest.IsPresent -FullUri | Select-Object -First $Limit
    }
}


function Get-MailMessageAttachment {
    <#
    .SYNOPSIS
    Get mail message attachment from Office 365 Graph API using UserPrincipalName and MessageId
 
    .DESCRIPTION
    Get mail message attachment from Office 365 Graph API using UserPrincipalName and MessageId
    Usually should be used with Get-MailMessage to get MessageId.
 
    .PARAMETER UserPrincipalName
    UserPrincipalName of the mailbox to get attachments from
 
    .PARAMETER Id
    MessageId of the message to get attachments from
 
    .PARAMETER Credential
    Credential parameter is used to securely pass tokens/api keys for Graph API
 
    .PARAMETER Property
    Property parameter is used to select which properties to return.
    By default if Path is specified, the file is saved and the file object is returned.
    However if Path is not specified, the data is returned from Graph API.
 
    .PARAMETER MgGraphRequest
    Forces using Invoke-MgGraphRequest internally.
    This allows to use Connect-MgGraph to authenticate and then use Get-MailMessageAttachment without any additional parameters.
 
    .PARAMETER Path
    Path parameter is used to specify where to save the attachment.
 
    .EXAMPLE
 
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $UserPrincipalName,
        [Parameter(Mandatory)][alias('MessageID')][string] $Id,
        [PSCredential] $Credential,
        [string[]] $Property,
        [switch] $MgGraphRequest,
        [string] $Path
    )

    if ($MgGraphRequest) {
        # do nothing, as we're using Connect-MgGraph
    } else {
        if ($Credential) {
            $AuthorizationData = ConvertFrom-GraphCredential -Credential $Credential
        } else {
            return
        }
        if ($AuthorizationData.ClientID -eq 'MSAL') {
            $Authorization = Connect-O365GraphMSAL -ApplicationKey $AuthorizationData.ClientSecret
        } else {
            $Authorization = Connect-O365Graph -ApplicationID $AuthorizationData.ClientID -ApplicationKey $AuthorizationData.ClientSecret -TenantDomain $AuthorizationData.DirectoryID -Resource https://graph.microsoft.com
        }
    }

    $QueryParameters = [ordered] @{
        select = $Property -join ','
    }
    $joinUriQuerySplat = @{
        BaseUri               = 'https://graph.microsoft.com/v1.0'
        QueryParameter        = $QueryParameters
        RelativeOrAbsoluteUri = "/users/$UserPrincipalName/messages/$Id/attachments"
    }
    Remove-EmptyValue -Hashtable $joinUriQuerySplat -Recursive -Rerun 2

    $Uri = Join-UriQuery @joinUriQuerySplat

    Write-Verbose "Get-MailMessageAttachment - Executing $Uri"

    $OutputData = Invoke-O365Graph -Headers $Authorization -Uri $Uri -Method GET -MGGraphRequest:$MgGraphRequest.IsPresent -FullUri

    if ($OutputData) {
        foreach ($Data in $OutputData) {
            if ($Data.contentBytes -and $Path) {
                try {
                    $PathToFile = [System.IO.Path]::Combine($Path, $Data.Name)
                    $FileStream = [System.IO.FileStream]::new($PathToFile, [System.IO.FileMode]::Create)
                    $AttachedBytes = [System.Convert]::FromBase64String($Data.ContentBytes)
                    $FileStream.Write($AttachedBytes, 0, $AttachedBytes.Length)
                    $FileStream.Close()
                    Get-Item -Path $PathToFile
                } catch {
                    Write-Warning "Get-MailMessageAttachment - Couldn't save file to $Path. Error: $($_.Exception.Message)"
                }
            } else {
                $Data
            }
        }
    } else {
        Write-Verbose "Get-MailMessageAttachment - No data found"
    }
}
function Get-POPMessage {
    [alias('Get-POP3Message')]
    [cmdletBinding()]
    param(
        [Parameter()][System.Collections.IDictionary] $Client,
        [int] $Index,
        [int] $Count = 1,
        [switch] $All
    )
    if ($Client -and $Client.Data) {
        if ($All) {
            $Client.Data.GetMessages($Index, $Count)
        } else {
            if ($Index -lt $Client.Data.Count) {
                $Client.Data.GetMessages($Index, $Count)
            } else {
                Write-Warning "Get-POP3Message - Index is out of range. Use index less than $($Client.Data.Count)."
            }
        }
    } else {
        Write-Warning 'Get-POP3Message - Is POP3 connected?'
    }
    <#
    $Client.Data.GetMessage
    MimeKit.MimeMessage GetMessage(int index, System.Threading.CancellationToken cancellationToken, MailKit.ITransferProgress progress)
    MimeKit.MimeMessage IMailSpool.GetMessage(int index, System.Threading.CancellationToken cancellationToken, MailKit.ITransferProgress progress)
    #>

    <#
    $Client.Data.GetMessages
    System.Collections.Generic.IList[MimeKit.MimeMessage] GetMessages(System.Collections.Generic.IList[int] indexes, System.Threading.CancellationToken cancellationToken, MailKit.ITransferProgress progress)
    System.Collections.Generic.IList[MimeKit.MimeMessage] GetMessages(int startIndex, int count, System.Threading.CancellationToken cancellationToken, MailKit.ITransferProgress progress)
    System.Collections.Generic.IList[MimeKit.MimeMessage] IMailSpool.GetMessages(System.Collections.Generic.IList[int] indexes, System.Threading.CancellationTokencancellationToken, MailKit.ITransferProgress progress)
    System.Collections.Generic.IList[MimeKit.MimeMessage] IMailSpool.GetMessages(int startIndex, int count, System.Threading.CancellationToken cancellationToken, MailKit.ITransferProgress progress)
    #>

}
function Import-MailFile {
    <#
    .SYNOPSIS
    Load .msg or .eml file as PowerShell object
 
    .DESCRIPTION
    Load .msg or .eml file as PowerShell object
 
    .PARAMETER InputPath
    Path to the .msg file
 
    .EXAMPLE
    $Msg = Import-MailFile -FilePath "$PSScriptRoot\Input\TestMessage.msg"
    $Msg | Format-Table
 
    .EXAMPLE
    $Eml = Import-MailFile -FilePath "$PSScriptRoot\Input\Sample.eml"
    $Eml | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][alias('FilePath', 'Path')][string] $InputPath
    )
    if ($InputPath -and (Test-Path -LiteralPath $InputPath)) {
        Try {
            $Item = Get-Item -LiteralPath $InputPath -ErrorAction Stop
        } catch {
            Write-Warning -Message "Import-MailFile - File $FilePath doesn't exist. Error: $_.Exception.Message"
            return
        }
        try {
            if ($Item.Extension -eq '.msg') {
                $Message = [MsgReader.Outlook.Storage+Message]::new($InputPath)
                $Message
            } elseif ($Item.Extension -eq '.eml') {
                $Message = [MsgReader.Mime.Message]::Load($InputPath)
                $Message
            } else {
                Write-Warning -Message "Import-MailFile - File $FilePath is not a .msg or .eml file."
            }
        } catch {
            Write-Warning -Message "Import-MailFile - File $FilePath is not a .msg or .eml file or another error occured. Error: $_.Exception.Message"
        }
    } else {
        Write-Warning -Message "Import-MailFile - File $FilePath doesn't exist."
    }
}
function Resolve-DnsQuery {
    [cmdletBinding()]
    param(
        [alias('Query')][Parameter(Mandatory)][string] $Name,
        [Parameter(Mandatory)][DnsClient.QueryType] $Type,
        [string] $Server,
        [switch] $All
    )
    if ($Server) {
        if ($Server -like '*:*') {
            $SplittedServer = $Server.Split(':')
            [System.Net.IPAddress] $IpAddress = $SplittedServer[0]
            $EndPoint = [System.Net.IPEndPoint]::new($IpAddress, $SplittedServer[1]) ##(IPAddress.Parse("127.0.0.1"), 8600);
        } else {
            [System.Net.IPAddress] $IpAddress = $Server
            $EndPoint = [System.Net.IPEndPoint]::new($IpAddress, 53) ##(IPAddress.Parse("127.0.0.1"), 8600);
        }
        $Lookup = [DnsClient.LookupClient]::new($EndPoint)
    } else {
        $Lookup = [DnsClient.LookupClient]::new()
    }
    if ($Type -eq [DnsClient.QueryType]::PTR) {
        #$Lookup = [DnsClient.LookupClient]::new()
        $Results = $Lookup.QueryReverseAsync($Name) | Wait-Task
        $Name = $Results.Answers.DomainName.Original
    }
    $Results = $Lookup.Query($Name, $Type)
    if ($All) {
        $Results
    } else {
        $Results.Answers
    }
}
function Resolve-DnsQueryRest {
    <#
    .SYNOPSIS
    Provides basic DNS Query via HTTPS
 
    .DESCRIPTION
    Provides basic DNS Query via HTTPS - tested only for use cases within Mailozaurr
 
    .PARAMETER DNSProvider
    Allows to choose DNS Provider that will be used for HTTPS based DNS query (Cloudlare or Google). Default is Cloudflare
 
    .PARAMETER Name
    Name/DomainName to query DNS
 
    .PARAMETER Type
    Type of a query A, PTR, MX and so on
 
    .PARAMETER All
    Returns full output rather than just custom, translated data
 
    .EXAMPLE
    Resolve-DnsQueryRest -Name 'evotec.pl' -Type TXT -DNSProvider Cloudflare
 
    .NOTES
    General notes
    #>

    [alias('Resolve-DnsRestQuery')]
    [cmdletBinding()]
    param(
        [alias('Query')][Parameter(Mandatory, Position = 0)][string] $Name,
        [Parameter(Mandatory, Position = 1)][string] $Type,
        [ValidateSet('Cloudflare', 'Google')][string] $DNSProvider = 'Cloudflare',
        [switch] $All
    )
    if ($Type -eq 'PTR') {
        $Name = $Name -replace '^(\d+)\.(\d+)\.(\d+)\.(\d+)$', '$4.$3.$2.$1.in-addr.arpa'
    }
    if ($DNSProvider -eq 'Cloudflare') {
        $Q = Invoke-RestMethod -Uri "https://cloudflare-dns.com/dns-query?name=$Name&type=$Type" -Headers @{ accept = 'application/dns-json' }
    } else {
        $Q = Invoke-RestMethod -Uri "https://dns.google.com/resolve?name=$Name&type=$Type"
    }
    $Answers = foreach ($Answer in $Q.Answer) {
        if ($Type -eq 'MX') {
            $Data = $Answer.data -split ' '
            [PSCustomObject] @{
                Name       = $Answer.Name
                Count      = $Answer.Type
                TimeToLive = $Answer.TTL
                Exchange   = $Data[1]
                Preference = $Data[0]
            }
        } elseif ($Type -eq 'A') {
            [PSCustomObject] @{
                Name       = $Answer.Name
                Count      = $Answer.Type
                TimeToLive = $Answer.TTL
                Address    = $Answer.data #.TrimStart('"').TrimEnd('"')
            }
        } else {
            [PSCustomObject] @{
                Name       = $Answer.Name
                Count      = $Answer.Type
                TimeToLive = $Answer.TTL
                Text       = $Answer.data.TrimStart('"').TrimEnd('"')
            }
        }
    }
    if ($All) {
        # TC If true, it means the truncated bit was set. This happens when the DNS answer is larger than a single UDP or TCP packet. TC will almost always be false with Cloudflare DNS over HTTPS because Cloudflare supports the maximum response size.
        # RD If true, it means the Recursive Desired bit was set. This is always set to true for Cloudflare DNS over HTTPS.
        # RA If true, it means the Recursion Available bit was set. This is always set to true for Cloudflare DNS over HTTPS.
        # AD If true, it means that every record in the answer was verified with DNSSEC.
        # CD If true, the client asked to disable DNSSEC validation. In this case, Cloudflare will still fetch the DNSSEC-related records, but it will not attempt to validate

        $Output = [ordered] @{}
        foreach ($Name in $Q.PSObject.Properties.Name) {
            if ($Name -eq 'Answer') {
                continue 
            }
            $Output[$Name] = $Q.$Name
        }
        $Output['NameServer'] = if ($DNSProvider -eq 'Cloudflare') {
            'cloudflare-dns.com' 
        } else {
            'dns.google.com' 
        }
        $Output['Answers'] = $Answers
        [PSCustomObject] $Output
    } else {
        $Answers
    }
}

Register-ArgumentCompleter -CommandName Resolve-DnsQueryRest -ParameterName Type -ScriptBlock $Script:DNSQueryTypes
function Save-MailMessage {
    [cmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][PSCustomObject[]] $Message,
        [string] $Path
    )
    Begin {
        $ResolvedPath = Convert-Path -LiteralPath $Path
    }
    Process {
        if (-not $ResolvedPath) {
            return
        }
        foreach ($M in $Message) {
            if ($M) {
                if ($M.Body -and $M.Content) {
                    Write-Verbose "Processing $($M.changekey)"
                    $RandomFileName = [io.path]::GetRandomFileName()
                    $RandomFileName = [io.path]::ChangeExtension($RandomFileName, 'html')
                    $FilePath = [io.path]::Combine($ResolvedPath, $RandomFileName)
                    try {
                        $M.Body.Content | Out-File -FilePath $FilePath -ErrorAction Stop
                    } catch {
                        Write-Warning "Save-MailMessage - Coultn't save file to $FilePath. Error: $($_.Exception.Message)"
                    }
                } else {
                    Write-Warning "Save-MailMessage - Message doesn't contain Body property. Did you request it? (eTag: $($M.'@odata.etag')"
                }
            }
        }
    }
    End {
    }
}
function Save-POPMessage {
    [alias('Save-POP3Message')]
    [cmdletBinding()]
    param(
        [Parameter()][System.Collections.IDictionary] $Client,
        [Parameter(Mandatory)][int] $Index,
        [Parameter(Mandatory)][string] $Path #,
        # [int] $Count = 1,
        #[switch] $All
    )
    if ($Client -and $Client.Data) {
        if ($All) {
            # $Client.Data.GetMessages($Index, $Count)
        } else {
            if ($Index -lt $Client.Data.Count) {
                $Client.Data.GetMessage($Index).WriteTo($Path)
            } else {
                Write-Warning "Save-POP3Message - Index is out of range. Use index less than $($Client.Data.Count)."
            }
        }
    } else {
        Write-Warning 'Save-POP3Message - Is POP3 connected?'
    }
}
function Send-EmailMessage {
    <#
    .SYNOPSIS
    The Send-EmailMessage cmdlet sends an email message from within PowerShell.
 
    .DESCRIPTION
    The Send-EmailMessage cmdlet sends an email message from within PowerShell. It replaces Send-MailMessage by Microsoft which is deprecated.
 
    .PARAMETER Server
    Specifies the name of the SMTP server that sends the email message.
 
    .PARAMETER Port
    Specifies an alternate port on the SMTP server. The default value is 587.
 
    .PARAMETER From
    This parameter specifies the sender's email address.
 
    .PARAMETER ReplyTo
    This property indicates the reply address. If you don't set this property, the Reply address is same as From address.
 
    .PARAMETER Cc
    Specifies the email addresses to which a carbon copy (CC) of the email message is sent.
 
    .PARAMETER Bcc
    Specifies the email addresses that receive a copy of the mail but are not listed as recipients of the message.
 
    .PARAMETER To
    Specifies the recipient's email address. If there are multiple recipients, separate their addresses with a comma (,)
 
    .PARAMETER Subject
    The Subject parameter isn't required. This parameter specifies the subject of the email message.
 
    .PARAMETER Priority
    Specifies the priority of the email message. Normal is the default. The acceptable values for this parameter are Normal, High, and Low.
 
    .PARAMETER Encoding
    Specifies the type of encoding for the target file. It's recommended to not change it.
 
    The acceptable values for this parameter are as follows:
 
    default:
    ascii: Uses the encoding for the ASCII (7-bit) character set.
    bigendianunicode: Encodes in UTF-16 format using the big-endian byte order.
    oem: Uses the default encoding for MS-DOS and console programs.
    unicode: Encodes in UTF-16 format using the little-endian byte order.
    utf7: Encodes in UTF-7 format.
    utf8: Encodes in UTF-8 format.
    utf32: Encodes in UTF-32 format.
 
    .PARAMETER DeliveryNotificationOption
    Specifies the delivery notification options for the email message. You can specify multiple values. None is the default value. The alias for this parameter is DNO. The delivery notifications are sent to the address in the From parameter. Multiple options can be chosen.
 
    .PARAMETER DeliveryStatusNotificationType
    Specifies delivery status notification type. Options are Full, HeadersOnly, Unspecified
 
    .PARAMETER Credential
    Specifies a user account that has permission to perform this action. The default is the current user.
    Type a user name, such as User01 or Domain01\User01. Or, enter a PSCredential object, such as one from the Get-Credential cmdlet.
    Credentials are stored in a PSCredential object and the password is stored as a SecureString.
 
    Credential parameter is also use to securely pass tokens/api keys for Graph API/oAuth2/SendGrid
 
    .PARAMETER Username
    Specifies UserName to use to login to server
 
    .PARAMETER Password
    Specifies Password to use to login to server. This is ClearText option and should not be used, unless used with SecureString
 
    .PARAMETER SecureSocketOptions
    Specifies secure socket option: None, Auto, StartTls, StartTlsWhenAvailable, SslOnConnect. Default is Auto.
 
    .PARAMETER UseSsl
    Specifies using StartTLS option. It's recommended to leave it disabled and use SecureSocketOptions which should take care of all security needs
 
    .PARAMETER SkipCertificateRevocation
    Specifies to skip certificate revocation check
 
    .PARAMETER SkipCertificateValidatation
    Specifies to skip certficate validation. Useful when using IP Address or self-generated certificates.
 
    .PARAMETER HTML
    HTML content to send email
 
    .PARAMETER Text
    Text content to send email. With SMTP one can define both HTML and Text. For SendGrid and Office 365 Graph API only HTML or Text will be used with HTML having priority
 
    .PARAMETER Attachment
    Specifies the path and file names of files to be attached to the email message.
 
    .PARAMETER Timeout
    Maximum time to wait to send an email via SMTP
 
    .PARAMETER oAuth2
    Send email via oAuth2
 
    .PARAMETER Graph
    Send email via Office 365 Graph API
 
    .PARAMETER MgGraphRequest
    Send email via Microsoft Graph API using Invoke-MgGraphRequest internally.
    This allows to use Connect-MgGraph to authenticate and then use Send-EmailMessage without any additional parameters.
 
    .PARAMETER AsSecureString
    Informs command that password provided is secure string, rather than clear text
 
    .PARAMETER SendGrid
    Send email via SendGrid API
 
    .PARAMETER SeparateTo
    Option separates each To field into separate emails (sent as one query). Supported by SendGrid only! BCC/CC are skipped when this mode is used.
 
    .PARAMETER DoNotSaveToSentItems
    Do not save email to SentItems when sending with Office 365 Graph API
 
    .PARAMETER Email
    Compatibility parameter for Send-Email cmdlet from PSSharedGoods
 
    .PARAMETER Suppress
    Do not display summary in [PSCustomObject]
 
    .PARAMETER LogPath
    When defined save the communication with server to file
 
    .PARAMETER LogObject
    When defined save the communication with server to object as message property
 
    .PARAMETER LogConsole
    When defined display the communication with server to console
 
    .PARAMETER LogTimestamps
    Configures whether log should use timestamps
 
    .PARAMETER LogTimeStampsFormat
    Configures the format of the timestamps in the log file.
 
    .PARAMETER LogSecrets
    Configures whether log should include secrets
 
    .PARAMETER LogClientPrefix
    Sets log prefix for client to specific value.
 
    .PARAMETER LogServerPrefix
    Sets log prefix for server to specific value.
 
    .PARAMETER MimeMessagePath
    Adds ability to save email message to file for troubleshooting purposes
 
    .PARAMETER LocalDomain
    Specifies the local domain name.
 
    .PARAMETER RequestReadReceipt
    Specifies whether to request a read receipt for the email message when using Microsoft Graph API (Graph switch)
 
    .PARAMETER RequestDeliveryReceipt
    Specifies whether to request a delivery receipt for the email message when using Microsoft Graph API (Graph switch)
 
    .EXAMPLE
    if (-not $MailCredentials) {
        $MailCredentials = Get-Credential
    }
 
    Send-EmailMessage -From @{ Name = 'PrzemysÅ‚aw KÅ‚ys'; Email = 'przemyslaw.klys@test.pl' } -To 'przemyslaw.klys@test.pl' `
        -Server 'smtp.office365.com' -SecureSocketOptions Auto -Credential $MailCredentials -HTML $Body -DeliveryNotificationOption OnSuccess -Priority High `
        -Subject 'This is another test email'
 
    .EXAMPLE
    if (-not $MailCredentials) {
        $MailCredentials = Get-Credential
    }
    # this is simple replacement (drag & drop to Send-MailMessage)
    Send-EmailMessage -To 'przemyslaw.klys@test.pl' -Subject 'Test' -Body 'test me' -SmtpServer 'smtp.office365.com' -From 'przemyslaw.klys@test.pl' `
        -Attachments "$PSScriptRoot\README.MD" -Cc 'przemyslaw.klys@test.pl' -Priority High -Credential $MailCredentials `
        -UseSsl -Port 587 -Verbose
 
    .EXAMPLE
    # Use SendGrid Api
    $Credential = ConvertTo-SendGridCredential -ApiKey 'YourKey'
 
    Send-EmailMessage -From 'przemyslaw.klys@evo.cool' `
        -To 'przemyslaw.klys@evotec.pl', 'evotectest@gmail.com' `
        -Body 'test me PrzemysÅ‚aw KÅ‚ys' `
        -Priority High `
        -Subject 'This is another test email' `
        -SendGrid `
        -Credential $Credential `
        -Verbose
 
    .EXAMPLE
    # It seems larger HTML is not supported. Online makes sure it uses less libraries inline
    # it may be related to not escaping chars properly for JSON, may require investigation
    $Body = EmailBody {
        EmailText -Text 'This is my text'
        EmailTable -DataTable (Get-Process | Select-Object -First 5 -Property Name, Id, PriorityClass, CPU, Product)
    } -Online
 
    # Credentials for Graph
    $ClientID = '0fb383f1'
    $DirectoryID = 'ceb371f6'
    $ClientSecret = 'VKDM_'
 
    $Credential = ConvertTo-GraphCredential -ClientID $ClientID -ClientSecret $ClientSecret -DirectoryID $DirectoryID
 
    # Sending email
    Send-EmailMessage -From @{ Name = 'PrzemysÅ‚aw KÅ‚ys'; Email = 'przemyslaw.klys@test1.pl' } -To 'przemyslaw.klys@test.pl' `
        -Credential $Credential -HTML $Body -Subject 'This is another test email 1' -Graph -Verbose -Priority High
 
    .EXAMPLE
    # Using OAuth2 for Office 365
    $ClientID = '4c1197dd-53'
    $TenantID = 'ceb371f6-87'
 
    $CredentialOAuth2 = Connect-oAuthO365 -ClientID $ClientID -TenantID $TenantID
 
    Send-EmailMessage -From @{ Name = 'PrzemysÅ‚aw KÅ‚ys'; Email = 'test@evotec.pl' } -To 'test@evotec.pl' `
        -Server 'smtp.office365.com' -HTML $Body -Text $Text -DeliveryNotificationOption OnSuccess -Priority High `
        -Subject 'This is another test email' -SecureSocketOptions Auto -Credential $CredentialOAuth2 -oAuth2
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'Compatibility', SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName = 'SecureString')]

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [alias('SmtpServer')][string] $Server,

        [Parameter(ParameterSetName = 'SecureString')]

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [int] $Port = 587,

        [Parameter(Mandatory, ParameterSetName = 'SecureString')]
        [Parameter(Mandatory, ParameterSetName = 'oAuth')]
        [Parameter(Mandatory, ParameterSetName = 'Graph')]
        [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')]
        [Parameter(Mandatory, ParameterSetName = 'Compatibility')]
        [Parameter(Mandatory, ParameterSetName = 'SendGrid')]
        [object] $From,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string] $ReplyTo,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string[]] $Cc,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string[]] $Bcc,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string[]] $To,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string] $Subject,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')][string] $Encoding = 'Default',

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType,

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(Mandatory, ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(Mandatory, ParameterSetName = 'SendGrid')]
        [pscredential] $Credential,

        [Parameter(ParameterSetName = 'SecureString')]
        [string] $Username,

        [Parameter(ParameterSetName = 'SecureString')]
        [string] $Password,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [MailKit.Security.SecureSocketOptions] $SecureSocketOptions = [MailKit.Security.SecureSocketOptions]::Auto,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $UseSsl,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $SkipCertificateRevocation,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [alias('SkipCertificateValidatation')][switch] $SkipCertificateValidation,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [alias('Body')][string[]] $HTML,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string[]] $Text,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [alias('Attachments')][string[]] $Attachment,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [int] $Timeout = 12000,

        [Parameter(ParameterSetName = 'oAuth')]
        [alias('oAuth')][switch] $oAuth2,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $RequestReadReceipt,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $RequestDeliveryReceipt,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $Graph,

        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $MgGraphRequest,

        [Parameter(ParameterSetName = 'SecureString')]
        [switch] $AsSecureString,

        [Parameter(ParameterSetName = 'SendGrid')]
        [switch] $SendGrid,

        [Parameter(ParameterSetName = 'SendGrid')]
        [switch] $SeparateTo,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $DoNotSaveToSentItems,

        # Different feature set
        [Parameter(ParameterSetName = 'Grouped')]
        [alias('EmailParameters')][System.Collections.IDictionary] $Email,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Grouped')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [switch] $Suppress,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string[]] $LogPath,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $LogConsole,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $LogObject,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $LogTimestamps,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $LogTimeStampsFormat = "yyyy-MM-dd HH:mm:ss:fff",

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $LogSecrets,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $LogClientPrefix,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $LogServerPrefix,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $MimeMessagePath,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $LocalDomain
    )
    $StopWatch = [system.diagnostics.stopwatch]::StartNew()
    if ($Email) {
        # Following code makes sure both formats are accepted.
        if ($Email.EmailTo) {
            $EmailParameters = $Email.Clone()
        } else {
            $EmailParameters = @{
                EmailFrom                   = $Email.From
                EmailTo                     = $Email.To
                EmailCC                     = $Email.CC
                EmailBCC                    = $Email.BCC
                EmailReplyTo                = $Email.ReplyTo
                EmailServer                 = $Email.Server
                EmailServerPassword         = $Email.Password
                EmailServerPasswordAsSecure = $Email.PasswordAsSecure
                EmailServerPasswordFromFile = $Email.PasswordFromFile
                EmailServerPort             = $Email.Port
                EmailServerLogin            = $Email.Login
                EmailServerEnableSSL        = $Email.EnableSsl
                EmailEncoding               = $Email.Encoding
                EmailEncodingSubject        = $Email.EncodingSubject
                EmailEncodingBody           = $Email.EncodingBody
                EmailSubject                = $Email.Subject
                EmailPriority               = $Email.Priority
                EmailDeliveryNotifications  = $Email.DeliveryNotifications
                EmailUseDefaultCredentials  = $Email.UseDefaultCredentials
            }
        }
        $From = $EmailParameters.EmailFrom
        $To = $EmailParameters.EmailTo
        $Cc = $EmailParameters.EmailCC
        $Bcc = $EmailParameters.EmailBCC
        $ReplyTo = $EmailParameters.EmailReplyTo
        $Server = $EmailParameters.EmailServer
        $Password = $EmailParameters.EmailServerPassword
        # $EmailServerPasswordAsSecure = $EmailParameters.EmailServerPasswordAsSecure
        # $EmailServerPasswordFromFile = $EmailParameters.EmailServerPasswordFromFile
        $Port = $EmailParameters.EmailServerPort
        $Username = $EmailParameters.EmailServerLogin
        #$UseSsl = $EmailParameters.EmailServerEnableSSL
        $Encoding = $EmailParameters.EmailEncoding
        #$EncodingSubject = $EmailParameters.EmailEncodingSubject
        $Encoding = $EmailParameters.EmailEncodingBody
        $Subject = $EmailParameters.EmailSubject
        $Priority = $EmailParameters.EmailPriority
        $DeliveryNotificationOption = $EmailParameters.EmailDeliveryNotifications
        #$EmailUseDefaultCredentials = $EmailParameters.EmailUseDefaultCredentials
    } else {
        if ($null -eq $To -and $null -eq $Bcc -and $null -eq $Cc) {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error 'At least one To, CC or BCC is required.'
                return
            } else {
                Write-Warning 'Send-EmailMessage - At least one To, CC or BCC is required.'
                return
            }
        }
    }
    if ($MgGraphRequest) {
        $sendGraphMailMessageSplat = @{
            From                   = $From
            To                     = $To
            Cc                     = $CC
            Bcc                    = $Bcc
            Subject                = $Subject
            HTML                   = $HTML
            Text                   = $Text
            Attachment             = $Attachment
            MgGraphRequest         = $MgGraphRequest
            Priority               = $Priority
            ReplyTo                = $ReplyTo
            DoNotSaveToSentItems   = $DoNotSaveToSentItems.IsPresent
            StopWatch              = $StopWatch
            Suppress               = $Suppress.IsPresent
            RequestReadReceipt     = $RequestReadReceipt.IsPresent
            RequestDeliveryReceipt = $RequestDeliveryReceipt.IsPresent
        }
        Remove-EmptyValue -Hashtable $sendGraphMailMessageSplat
        return Send-GraphMailMessage @sendGraphMailMessageSplat
    }

    # lets define credentials early on, because if it's Graph we use different way to send emails
    if ($Credential) {
        if ($oAuth2.IsPresent) {
            $Authorization = ConvertFrom-OAuth2Credential -Credential $Credential
            $SaslMechanismOAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($Authorization.UserName, $Authorization.Token)
            $SmtpCredentials = $Credential
        } elseif ($Graph.IsPresent) {
            # Sending email via Office 365 Graph
            $sendGraphMailMessageSplat = @{
                From                   = $From
                To                     = $To
                Cc                     = $CC
                Bcc                    = $Bcc
                Subject                = $Subject
                HTML                   = $HTML
                Text                   = $Text
                Attachment             = $Attachment
                Credential             = $Credential
                Priority               = $Priority
                ReplyTo                = $ReplyTo
                DoNotSaveToSentItems   = $DoNotSaveToSentItems.IsPresent
                StopWatch              = $StopWatch
                Suppress               = $Suppress.IsPresent
                RequestReadReceipt     = $RequestReadReceipt.IsPresent
                RequestDeliveryReceipt = $RequestDeliveryReceipt.IsPresent
            }
            Remove-EmptyValue -Hashtable $sendGraphMailMessageSplat
            return Send-GraphMailMessage @sendGraphMailMessageSplat
        } elseif ($SendGrid.IsPresent) {
            # Sending email via SendGrid
            $sendGraphMailMessageSplat = @{
                From       = $From
                To         = $To
                Cc         = $CC
                Bcc        = $Bcc
                Subject    = $Subject
                HTML       = $HTML
                Text       = $Text
                Attachment = $Attachment
                Credential = $Credential
                Priority   = $Priority
                ReplyTo    = $ReplyTo
                SeparateTo = $SeparateTo.IsPresent
                StopWatch  = $StopWatch
                Suppress   = $Suppress.IsPresent
            }
            Remove-EmptyValue -Hashtable $sendGraphMailMessageSplat
            return Send-SendGridMailMessage @sendGraphMailMessageSplat
        } else {
            $SmtpCredentials = $Credential
        }
    } elseif ($Username -and $Password -and $AsSecureString) {
        # Convert to SecureString
        try {
            $secStringPassword = ConvertTo-SecureString -ErrorAction Stop -String $Password
            $SmtpCredentials = [System.Management.Automation.PSCredential]::new($UserName, $secStringPassword)
        } catch {
            Write-Warning "Send-EmailMessage - Couldn't translate secure string to password. Error $($_.Exception.Message)"
            return
        }
    } elseif ($Username -and $Password) {
        #void Authenticate(string userName, string password, System.Threading.CancellationToken cancellationToken)
    }

    $Message = [MimeKit.MimeMessage]::new()

    # Doing translation for compatibility with Send-MailMessage
    if ($Priority -eq 'High') {
        $Message.Priority = [MimeKit.MessagePriority]::Urgent
    } elseif ($Priority -eq 'Low') {
        $Message.Priority = [MimeKit.MessagePriority]::NonUrgent
    } else {
        $Message.Priority = [MimeKit.MessagePriority]::Normal
    }

    [MimeKit.InternetAddress] $SmtpFrom = ConvertTo-MailboxAddress -MailboxAddress $From
    $Message.From.Add($SmtpFrom)

    if ($To) {
        [MimeKit.InternetAddress[]] $SmtpTo = ConvertTo-MailboxAddress -MailboxAddress $To
        $Message.To.AddRange($SmtpTo)
    }
    if ($Cc) {
        [MimeKit.InternetAddress[]] $SmtpCC = ConvertTo-MailboxAddress -MailboxAddress $Cc
        $Message.Cc.AddRange($SmtpCC)
    }
    if ($Bcc) {
        [MimeKit.InternetAddress[]] $SmtpBcc = ConvertTo-MailboxAddress -MailboxAddress $Bcc
        $Message.Bcc.AddRange($SmtpBcc)
    }
    if ($ReplyTo) {
        [MimeKit.InternetAddress] $SmtpReplyTo = ConvertTo-MailboxAddress -MailboxAddress $ReplyTo
        $Message.ReplyTo.Add($SmtpReplyTo)
    }
    $MailSentTo = -join ($To -join ',', $CC -join ', ', $Bcc -join ', ')
    if ($Subject) {
        $Message.Subject = $Subject
    }

    [System.Text.Encoding] $SmtpEncoding = [System.Text.Encoding]::$Encoding

    $BodyBuilder = [MimeKit.BodyBuilder]::new()
    if ($HTML) {
        $BodyBuilder.HtmlBody = $HTML
    }
    if ($Text) {
        $BodyBuilder.TextBody = $Text
    }
    if ($Attachment) {
        foreach ($A in $Attachment) {
            $null = $BodyBuilder.Attachments.Add($A)
        }
    }
    $Message.Body = $BodyBuilder.ToMessageBody()

    ### SMTP Part Below
    $OutputMessage = $null
    if ($LogPath -or $LogConsole -or $LogObject) {
        if ($LogPath) {
            # we make sure to set it to false, just in case user provided both
            $LogObject = $false
            # if protocol logger fails to save, we need to do something with it
            try {
                $ProtocolLogger = [MailKit.ProtocolLogger]::new($LogPath)
            } catch {
                Write-Warning -Message "Send-EmailMessage - Couldn't create protocol logger with $LogPath. Error $($_.Exception.Message.Replace([System.Environment]::NewLine, " ")). Using console output instead."
                $ProtocolLogger = [MailKit.ProtocolLogger]::new([System.Console]::OpenStandardOutput())
            }
        } elseif ($LogConsole) {
            $ProtocolLogger = [MailKit.ProtocolLogger]::new([System.Console]::OpenStandardOutput())
        } else {
            $Stream = [System.IO.MemoryStream]::new()
            $ProtocolLogger = [MailKit.ProtocolLogger]::new($Stream)
        }
        $ProtocolLogger.LogTimestamps = $LogTimestamps.IsPresent
        $ProtocolLogger.RedactSecrets = -not $LogSecrets.IsPresent
        if ($LogTimeStampsFormat) {
            $ProtocolLogger.TimestampFormat = $LogTimeStampsFormat
        }
        if ($PSBoundParameters.Keys.Contains('LogServerPrefix')) {
            $ProtocolLogger.ServerPrefix = $LogServerPrefix
        }
        if ($PSBoundParameters.Keys.Contains('LogClientPrefix')) {
            $ProtocolLogger.ClientPrefix = $LogClientPrefix
        }
        $SmtpClient = [MySmtpClientWithLogger]::new($ProtocolLogger)
    } else {
        $SmtpClient = [MySmtpClient]::new()
    }
    if ($LocalDomain) {
        $SmtpClient.LocalDomain = $LocalDomain
    }
    if ($SkipCertificateRevocation) {
        $SmtpClient.CheckCertificateRevocation = $false
    }
    if ($SkipCertificateValidatation) {
        $SmtpClient.ServerCertificateValidationCallback = { $true }
    }
    if ($DeliveryNotificationOption) {
        # This requires custom class MySmtpClient
        $SmtpClient.DeliveryNotificationOption = $DeliveryNotificationOption
    }
    if ($DeliveryStatusNotificationType) {
        $SmtpClient.DeliveryStatusNotificationType = $DeliveryStatusNotificationType
    }
    if ($UseSsl) {
        # By default Auto is used, but if someone wants UseSSL that's fine too
        $SecureSocketOptions = [MailKit.Security.SecureSocketOptions]::StartTls
    }
    try {
        $SmtpClient.Connect($Server, $Port, $SecureSocketOptions)
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else {
            Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message)"
            Write-Warning "Send-EmailMessage - Possible issue: Port? ($Port was used), Using SSL? ($SecureSocketOptions was used). You can also try SkipCertificateValidation or SkipCertificateRevocation. "
            if (-not $Suppress) {
                if ($LogObject) {
                    $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                }
                return [PSCustomObject] @{
                    Status        = $False
                    Error         = $($_.Exception.Message)
                    SentTo        = $MailSentTo
                    SentFrom      = $From
                    Message       = $OutputMessage
                    TimeToExecute = $StopWatch.Elapsed
                    Server        = $Server
                    Port          = $Port
                }
            }
        }
    }
    if ($SmtpCredentials) {
        if ($oAuth2.IsPresent) {
            try {
                $SmtpClient.Authenticate($SaslMechanismOAuth2)
            } catch {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    Write-Error $_
                    return
                } else {
                    Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message)"
                    if (-not $Suppress) {
                        if ($LogObject) {
                            $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                        }
                        return [PSCustomObject] @{
                            Status        = $False
                            Error         = $($_.Exception.Message)
                            SentTo        = $MailSentTo
                            SentFrom      = $From
                            Message       = $OutputMessage
                            TimeToExecute = $StopWatch.Elapsed
                            Server        = $Server
                            Port          = $Port
                        }
                    }
                }
            }
        } elseif ($Graph.IsPresent) {
            # This is not going to happen is graph is used
        } else {
            try {
                $SmtpClient.Authenticate($SmtpEncoding, $SmtpCredentials, [System.Threading.CancellationToken]::None)
            } catch {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    Write-Error $_
                    return
                } else {
                    Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message)"
                    if (-not $Suppress) {
                        if ($LogObject) {
                            $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                        }
                        return [PSCustomObject] @{
                            Status        = $False
                            Error         = $($_.Exception.Message)
                            SentTo        = $MailSentTo
                            SentFrom      = $From
                            Message       = $OutputMessage
                            TimeToExecute = $StopWatch.Elapsed
                            Server        = $Server
                            Port          = $Port
                        }
                    }
                }
            }
        }
    } elseif ($UserName -and $Password) {
        try {
            $SmtpClient.Authenticate($UserName, $Password, [System.Threading.CancellationToken]::None)
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else {
                Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message)"
                if (-not $Suppress) {
                    if ($LogObject) {
                        $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                    }
                    return [PSCustomObject] @{
                        Status        = $False
                        Error         = $($_.Exception.Message)
                        SentTo        = $MailSentTo
                        SentFrom      = $From
                        Message       = $OutputMessage
                        TimeToExecute = $StopWatch.Elapsed
                        Server        = $Server
                        Port          = $Port
                    }
                }
            }
        }
    }
    $SmtpClient.Timeout = $Timeout
    try {
        if ($PSCmdlet.ShouldProcess("$MailSentTo", 'Send-EmailMessage')) {
            $OutputMessage = $SmtpClient.Send($Message)
            if (-not $Suppress) {
                if ($LogObject) {
                    $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                }
                [PSCustomObject] @{
                    Status        = $True
                    Error         = ''
                    SentTo        = $MailSentTo
                    SentFrom      = $From
                    Message       = $OutputMessage
                    TimeToExecute = $StopWatch.Elapsed
                    Server        = $Server
                    Port          = $Port
                }
            }
        } else {
            if (-not $Suppress) {
                if ($LogObject) {
                    $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
                }
                [PSCustomObject] @{
                    Status        = $false
                    Error         = 'Email not sent (WhatIf)'
                    SentTo        = $MailSentTo
                    SentFrom      = $From
                    Message       = $OutputMessage
                    TimeToExecute = $StopWatch.Elapsed
                    Server        = $Server
                    Port          = $Port
                }
            }
        }
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else {
            Write-Warning "Send-EmailMessage - Error: $($_.Exception.Message)"
        }
        if (-not $Suppress) {
            if ($LogObject) {
                $OutputMessage = [System.Text.Encoding]::ASCII.GetString($stream.ToArray());
            }
            [PSCustomObject] @{
                Status        = $False
                Error         = $($_.Exception.Message)
                SentTo        = $MailSentTo
                SentFrom      = $From
                Message       = $OutputMessage
                TimeToExecute = $StopWatch.Elapsed
                Server        = $Server
                Port          = $Port
            }
        }
    }
    $SmtpClient.Disconnect($true)
    if ($MimeMessagePath) {
        $Message.WriteTo($MimeMessagePath)
    }
    $StopWatch.Stop()
}
function Test-EmailAddress {
    <#
    .SYNOPSIS
    Checks if email address matches conditions to be valid email address.
 
    .DESCRIPTION
    Checks if email address matches conditions to be valid email address.
 
    .PARAMETER EmailAddress
    EmailAddress to check
 
    .EXAMPLE
    Test-EmailAddress -EmailAddress 'przemyslaw.klys@test'
 
    .EXAMPLE
    Test-EmailAddress -EmailAddress 'przemyslaw.klys@test.pl'
 
    .EXAMPLE
    Test-EmailAddress -EmailAddress 'przemyslaw.klys@test','przemyslaw.klys@test.pl'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][string[]] $EmailAddress
    )
    process {
        foreach ($Email in $EmailAddress) {
            [PSCustomObject] @{
                EmailAddress = $Email
                IsValid      = [EmailValidation.EmailValidator]::Validate($Email)
            }
        }
    }
}



# Export functions and aliases as required
Export-ModuleMember -Function @('Connect-IMAP', 'Connect-oAuthGoogle', 'Connect-oAuthO365', 'Connect-POP', 'ConvertFrom-EmlToMsg', 'ConvertTo-GraphCredential', 'ConvertTo-OAuth2Credential', 'ConvertTo-SendGridCredential', 'Disconnect-IMAP', 'Disconnect-POP', 'Find-BIMIRecord', 'Find-CAARecord', 'Find-DANERecord', 'Find-DKIMRecord', 'Find-DMARCRecord', 'Find-DNSBL', 'Find-DNSSECRecord', 'Find-IPGeolocation', 'Find-MTASTSRecord', 'Find-MxRecord', 'Find-O365OpenIDRecord', 'Find-SecurityTxtRecord', 'Find-SPFRecord', 'Find-TLSRPTRecord', 'Get-DMARCData', 'Get-IMAPFolder', 'Get-IMAPMessage', 'Get-MailFolder', 'Get-MailMessage', 'Get-MailMessageAttachment', 'Get-POPMessage', 'Import-MailFile', 'Resolve-DnsQuery', 'Resolve-DnsQueryRest', 'Save-MailMessage', 'Save-POPMessage', 'Send-EmailMessage', 'Test-EmailAddress') -Alias @('Connect-POP3', 'Disconnect-POP3', 'Find-BlackList', 'Find-BlockList', 'Find-O365TenantID', 'Get-IPGeolocation', 'Get-POP3Message', 'Resolve-DnsRestQuery', 'Save-POP3Message')
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBTuxnrVScf7mRp
# hJIxPmiXZaSPADY8mP6khXqTYDhuPKCCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCBgur0q2phrQFqLjNwLOZH2TuElhWHtdXNUXdjHuslwmTAN
# BgkqhkiG9w0BAQEFAASCAgAVGWXZWkcITWQpZAecsU50SU41u1sZrnH9Orl/fKI2
# ouRsaGg3NKbeuJKADkXXLW9NLBxEfs2CjSBf4fBfy8OShfjEQ1h/YwEo49y2X33h
# MEE2UxHTE329bLJWz6kXPT0tCqKh1JkzXQd3utUCkTLGsCpv5aM3dGQWPQyBAEFr
# +lG/PHCz/f49vIyuLABwKSfxp3DVmg4iDXULc7K1kM496Bf6gKp4qUhQpQcpPogg
# H9PvbIRJzPvsgCwAN947YMWpK8ZmN9OfnEsj8ucag5NpGXrqQ6EISxsDjHGbgBy4
# 9bKDZ83Kjosj0v5kXFGPYYeYTo7Ij4MefYlZPgPj7v7clXLgHPsgjTiOtl1o7C3O
# tRho5t7GaB312mxTPbeEsYll5H60BdKG/UNuLCtUYHUFbdDYQ4H84RduxaZhkTEU
# wQxqpniCsnm4zVHbKrF3FK1ZHfE3/Z82aB91Jtzi3G43mkxfDmPLo9mO5EUhCAma
# UycDFv7rUUqr8+eXTMZUiLHHGwfAOp8gXPWTVa2BF1Ltxp/Sv7CI/LEgPw1PneXx
# mQUSOVMQCU3bxkhUvnmqh8SFkK7Hhbl7m+9JPuPxcJAwQwTNXttHaz76b4mJJ4x6
# QikauYqzSLRPCf8TK+6/n0F9EdxLYb+pjAJKKb9LUOFzdmuuvS1vxr/dFBZZwHwI
# tKGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDAzMDQxNzE5NDhaMC8GCSqGSIb3DQEJBDEi
# BCAdBz2oxoEXoUQoUI3rG/MHKaQjGYF6UrdmaGY0G4oi/TANBgkqhkiG9w0BAQEF
# AASCAgBtA03h6gmnQ+6264afO/X/n8ujppzDS3Pm30Gj5MoRl4MzWCEVdw9VjJPS
# XhUgwTuo7ggcf+vntNnzPw+h0/liMqpoebTQP7ek/p+kNdocrtSRwERJmvnQsZpk
# 9i55jpAFTPHyUktfuK5eEeJcipFmjHga1qopErtV2b7EyR+8rCUJzhA9TyItsmhZ
# XIaczI/OaIuFA6rrI2ajipXdd5WWTlkN34AjAXc7jT71bfIXCHrA9GT2fUbwt2Mz
# x0RM/GbhH5h/pW0HpbAfkCapYFncTzYY8+HFooUvN6zVXLn0HRK8XUTxQ3Wv7x7t
# QRQyFEFqOosU+MsZBTOuXXuqnWyYco+XxSHC/yG0ITIi+qZx9q3vPCUVJL/SGyc0
# wEy1Eeo5xo7e3axIRm2F7UTP4xySQoXYGKkFYGXvtXsOAgkC/6qLOadzgcInu3GG
# gHysC/0opsjbUXHpI4/a03neHJ4Rb1SpS9WrTn0uxmIVOduromewnv4Kt53jFBvq
# TQqVz9smINx36WeQiT80lHESn57y0zFzUHzaCC77pz1NCUrzOVMnUbqjz1h9Spiw
# SiOpbaoypfhPyvLkbkySvvGO6diD8XXXKoNcC13Vx7P1ztIzYJ8iEX6cwSG4ivUW
# r4MTmEGYkWGyn9sP7vSRbtob1YuIJsytG9Cuk5AYmLjRUKZGqA==
# SIG # End signature block