Mailozaurr.psm1
# 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 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 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 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 ) $RestSplat = @{ Headers = $Headers Method = $Method ContentType = $ContentType } if ($FullUri) { $RestSplat.Uri = $Uri } else { $RestSplat.Uri = -join ($PrimaryUri, $Uri) } try { $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false if ($Method -eq 'GET') { if ($OutputQuery.value) { $OutputQuery.value } if ($OutputQuery.'@odata.nextLink') { $RestSplat.Uri = $OutputQuery.'@odata.nextLink' $MoreData = Invoke-O365Graph @RestSplat -FullUri if ($MoreData) { $MoreData } } } else { return $true } } catch { $RestError = $_.ErrorDetails.Message if ($RestError) { try { $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 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-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 ) 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 } } } catch { $MailRecord = [ordered] @{ Name = $D Count = 0 TimeToLive = '' DMARC = '' QueryServer = '' } Write-Warning "Find-DMARCRecord - $_" } 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-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-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 ) 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 (-not $AsObject) { $MailRecord = [ordered] @{ Name = $D Count = $DNSRecordAnswers.Count TimeToLive = $DNSRecordAnswers.TimeToLive -join '; ' SPF = $DNSRecordAnswers.Text -join '; ' QueryServer = $DNSRecord.NameServer } } else { $MailRecord = [ordered] @{ Name = $D Count = $DNSRecordAnswers.Count TimeToLive = $DNSRecordAnswers.TimeToLive SPF = $DNSRecordAnswers.Text QueryServer = $DNSRecord.NameServer } } } catch { $MailRecord = [ordered] @{ Name = $D Count = 0 TimeToLive = '' SPF = '' QueryServer = '' } Write-Warning "Find-SPFRecord - $_" } if ($AsHashTable) { $MailRecord } else { [PSCustomObject] $MailRecord } } } } 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 { [cmdletBinding()] param( [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 ) 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 } $Uri = "/users/$UserPrincipalName/messages" $Addon = '?' if ($Property) { $Poperties = $Property -join ',' $Addon = -join ($Addon, "`$Select=$Poperties") } if ($Filter) { $Addon = -join ($Addon, "&`$filter=$Filter") } #Write-Verbose $Addon #$Addon = [System.Web.HttpUtility]::UrlEncode($Addon) if ($Addon.Length -gt 1) { $Uri = -join ($Uri, $Addon) } Write-Verbose "Get-MailMessage - Executing $Uri" $Uri = [uri]::EscapeUriString($Uri) Write-Verbose "Get-MailMessage - Executing $Uri" if ($All) { Invoke-O365Graph -Headers $Authorization -Uri $Uri -Method GET } else { Invoke-O365Graph -Headers $Authorization -Uri $Uri -Method GET | Select-Object -First $Limit } } 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 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 #> [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) { [PSCustomObject] @{ NameServer = if ($DNSProvider -eq 'Cloudflare') { 'cloudflare-dns.com' } else { 'dns.google.com' } Answers = $Answers } } 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', 'ConvertTo-GraphCredential', 'ConvertTo-OAuth2Credential', 'ConvertTo-SendGridCredential', 'Disconnect-IMAP', 'Disconnect-POP', 'Find-DKIMRecord', 'Find-DMARCRecord', 'Find-DNSBL', 'Find-MxRecord', 'Find-SPFRecord', 'Get-IMAPFolder', 'Get-IMAPMessage', 'Get-MailFolder', 'Get-MailMessage', 'Get-POPMessage', 'Resolve-DnsQuery', 'Resolve-DnsQueryRest', 'Save-MailMessage', 'Save-POPMessage', 'Send-EmailMessage', 'Test-EmailAddress') -Alias @('Connect-POP3', 'Disconnect-POP3', 'Find-BlackList', 'Find-BlockList', 'Get-POP3Message', 'Save-POP3Message') # SIG # Begin signature block # MIItrwYJKoZIhvcNAQcCoIItoDCCLZwCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCsphV51IFz7p82 # fCGmFmq17H2xequkEhuQIFhDHvgywqCCJrIwggWNMIIEdaADAgECAhAOmxiO+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 # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwDCCBKigAwIBAgIQDE1pckuU+jwqSj0p # B4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAwMDAwMFoXDTMzMTEy # MTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSQwIgYD # VQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSFdDMaJqzQHFUeHjZt # vJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWvM+xhiummKNuQY1y9 # iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyIxvG+4C99O7HKU41A # gx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3UTZWEaOOAy2p50dIQ # kUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyVR4aFeT4MXmaMGgok # vpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQln5N4d3CraV++C0b # H+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq5Xwx5/PCUsTmFnta # fqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk+lbP4PQK5hRtZHi7 # mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl5S4pkKa3YWT62SBs # GFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7ucxnEweawXjtxojI # sG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076XepFcxyEftfO4tQ6 # dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYD # VR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI # AYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQW # BBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2 # VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hB # MjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBVqioa80bz # eFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4iGNVCUY5APxp1Mqb # KfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIrUPwbtZ4IMAn65C3X # CYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk/9+dEKfrALpfSo8a # OlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+YsiaVOBmIRBTlClmi # a+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YBZJwAwuladHUNPeF5 # iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD+5sTX2q1x+DzBcNZ # 3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQRWAzgOAj3vgDpPZF # R+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+bvdgcmlHEL5r2X6cn # l7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTontRamMifv427GFxD # 9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/Otrl5fbmm9x+LMz/ # F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDCCB18wggVHoAMCAQICEAfCUnQoFKLW # q/4k6hfl3S4wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoT # DkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENv # ZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAeFw0yMzA0MTYwMDAw # MDBaFw0yNjA3MDYyMzU5NTlaMGcxCzAJBgNVBAYTAlBMMRIwEAYDVQQHDAlNaWtv # xYLDs3cxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVWT1RFQzEhMB8GA1UE # AwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAlJoHlzELSGimkpCr2wLfBhWSdcsDh/EsMZU7rODHMq1plTq0 # QVUUAPAKRfRWnqG8JpGcb5MUExSxypvvJJ8KJhFLJXGvAqkjiNGMBC7+RME1RIdA # vw2nob8aOrZJjTxff0j9Sgt3NJdbzvjO73TVRikCEK4cauxBtInswWTgIrpDXRlV # 0WDi5+O1d6i+T8Bv6LtmpSf74nyA2nfNahW/kJFIdNiaNuEjI1nSg8rXazF4tNt+ # QjeEa1vvII30Sfnyio4DCJm7nHgrIvSL9Wuum1HPWpwHpjm0+JheVP8kAYALgKN/ # o1QfMIlHfO5FEDtMyQhfL6tmK1Ts/DiZjF/IICLBBFGdwmSg9IVXN3Zu3FkgMPPx # TcxjT5QGiMc11/ang9BIGgi0ZCLQN7d3kFviAF8kv/WZ56RVKA70BmyvkOP2z9Im # /fFy30KcVRkbtHAldDYO+wyJERfiMkdT3MFQKvjs1VN7ynqNub/657YlwpgsYluK # B2DtvHkkP3iAHJ4ovt7igzWayNeT+1cQ65FCHOhbYkrzocHNwM2PrxH4r1JBSkas # L0kq+Hwq65JO89kHu9mcJcNhA0VR8stH1FRjvUDLoehN0cJyS/eoqdGpXJoSgARq # CKkltOZ13QlG5F5oTwk0+Z2kA7mdVJAF22T0oSo2z8M3Vz9m/CPZ0PPVUoECAwEA # AaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1Ud # DgQWBBR68WolWbgyccRJNeWy6DLhSOdt9zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l # BAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2 # U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFD # QTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6 # Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYB # BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0 # cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNp # Z25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkqhkiG # 9w0BAQsFAAOCAgEAtxHh11D4aXt9Stgy+Nx34eqpLwR8kdUZQ/ZVSJJXEQkedGR8 # 6FrOhAZUxcqIb5KXJVQrkXUFt97Uur7SjzrnKQw7+MLAPus5CWCPHx6Lluk6mtVu # O2Eq3OQDkoSHCffjaTWyjRood3aEpXIqNplCgl+SP2a8yQZEKSdJGIWv6VEk9gmx # Nya6CX9r0FhlIiPidy3YjzR5oTtZfs2kJEsb9HFQxEzH0BmSikVREmehYOtW9HY7 # 0EseddDHW8bSjI70t2bQMrap0B5NYqT/kYPjOZRR60pFJZ6Rmvn957kIcQ2+zfRP # IVFXr8QC06xYn4PM4bJVUR+fw3/wsZTClwu6Kd9PwMkLDkMR1tbjcd7RtQzIIs6c # AWrK8YesGu4mgPi6dO6tSPdni4a2G7cN8QtrzSBnTHTe53e+sjCI3WJwJ+69/MML # WidymA9EE5e+xAfLv+XArN0oWXQ3coOCuzaCZfIhB626raKABzjC4iaYi9ovWJ/J # EDAev0OkTDtyFDy7snAfaOgzYsEB3+ibeaFuz9PZOTccQRJpLMcDW5mbzUOuWZ93 # sVACqhvsd9RIM+SGeFP4z80WpRJRCKUtK4K1YPEfKRDoXfeZhM6eVhEShcl4Xupw # em0mB7/HJSwFdIjJLt9PK6X4zIkJKktyy831CeTh6rSikDTC8c/c9fOVArMxggZT # MIIGTwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j # LjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNB # NDA5NiBTSEEzODQgMjAyMSBDQTECEAfCUnQoFKLWq/4k6hfl3S4wDQYJYIZIAWUD # BAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMx # DAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkq # hkiG9w0BCQQxIgQgxwWdPWcuZRFFTMlskjyWFPSWWi7DWRvk1pqtc3xzjVAwDQYJ # KoZIhvcNAQEBBQAEggIAcAcZJLamSWoWw7AbMBBuUPpmXCnM78yQJlytwg1f3Odz # /ATm531I2dqzJJLqo54mO+wQB2bkFQU8TqVBZP4zfOaDpMpOLA87CXSQyZU29UvJ # MFD2UNwFcny6bZGwRlsSZSemyjGX/ElkjXTuKRgTt+kkaIhARfL2w8G7P12Whbvb # LTsf+l20dXvXxjNP1Q9LEdov7Lruxh7UmCuDasQjBRMhoiJIrbEHJnRLtw6Atc/d # HwllzFK2sJIEYwtYK4dNFBq2Geo6VoFxc4iEk4dAR4ig5RwX3+6IJgxQ24Ed37VS # l7p3hznNWM3ZZxw3xzZ3NZ0AgjUqY1L9fsg8uAsgw9IPt3urEVu0OywP1Jy5Cg6g # BmWmNe4zGW9Jb0OHO/iwx5X6ctlVNydy2HGrjCUHtHaTzLcEfbm59OrGRlS8S/T1 # 1JonSd/pFeXG9LIj8oYCV8kShk0V96WyqWvBDnFR+q9bZ2RKyWjUmQwA034h1Gqf # V5Qv6oZd5E9ti5p2+qSO38vvAH5KX6wZ+VgKp6Yw4g80SAkvS8oUn8jZNLFoFrQT # Ot9Ysx/KWrRI/9DSpQ+4mBEnrWAEGoLtD2F2ZKussqOkzidpEXgMRI3fUV3OAnk/ # NlER/kWu+nDLsZpJJmTz/kZJKL1rb/HNP2vMDaxvBCQP6cScIunBWWPWHe28OvKh # ggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAMTWlyS5T6PCpK # PSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEH # ATAcBgkqhkiG9w0BCQUxDxcNMjMwODA1MDcwMDM1WjAvBgkqhkiG9w0BCQQxIgQg # 7wGkOFSwt2TP1hfzOfZETSRGp+vh8TmX+bLR4viB9PkwDQYJKoZIhvcNAQEBBQAE # ggIAgJJIBTb9o6LCb6dxQBNsSg9b8doUloZPdfVPU5oESCXlhOEIsJi33TkvilWX # Qxm1ydqwYYFGVgTkAkUJzMBlvUqfrp+7yRlP+Sy19WHGJJwlKVcYrWRZjm/3DWAb # vR1hZ44Wz1BGRj+rFN6QH/8rTb3rVCGYRPNRU9JTei25sEC5a9QeI++fV9P5J+XP # WyLsLTWVJhhshynA/GX8fQ2H4a5LiDmi7ygVhB3dTiauvl/Q/z9wMM4bRa6nGCRI # VOEEjVZhWf39gUnMMdaEt6azEobfz8yUPH6EdOL2rt1eho07v5Tt1SEy3v22gYWW # sO9CjisVBhvkRhY1X/nwHjMgu7NfWm7sD2e/HqEz6oOtCVCWUpbigE8NlePpEI4y # 8OExQKMwtLNuPMSO34QaF+UVDp6vBuTqAq6sDlxn4BodfmsujIvtutd8h88p5yFO # WbZOLPju98HJOZxltMHivpU/fjl/C4/UghCR4EWuzMWVBoDG79d8g1NT8QMY2+Of # vHkWyBLIopU5VNTk+j4J7lTqDWZqxciVg2RKicwejii5DO5BlLeSZDvxvUtRZvNa # 4hgHtt8gp37asrB0RPQdSxiU++gJO3HFimLEY83wF/LSHxGcAXrA78fIEuMOLgYm # 69guEIInOzU+yOB3XL20Ee9Hsr6vX1oRa1Zzhzhap5VaR/k= # SIG # End signature block |