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 |