Public/Helpers.ps1
<#
.SYNOPSIS Test Exchange Online connectivity .DESCRIPTION When staying in a Powershell Session with Exchange Online many things can occur to disturb the session. The Test-SC365connectivity CmdLet figures out if the session is still valid .EXAMPLE PS C:\> Test-SC365ConnectionStatus Whithout any parameter the CmdLet emits just true or false .EXAMPLE PS C:\> Test-SC365ConnectionStatus -verbose For deeper analisys of connectivity issues the verbose switch provides a lot of relevant information. .EXAMPLE PS C:\> Test-SC365ConnectionStatus -showDefaultDomain ShowDefaultdomain will also emit the current default e-mail domain .EXAMPLE PS C:\> Test-SC365ConnectionStatus -Connect Connnect will try to connect via the standard method (web-browser) .INPUTS Inputs (if any) .OUTPUTS true/false .NOTES See https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md for more #> function Test-SC365ConnectionStatus { [CmdLetBinding( HelpURI = 'https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md#setup-the-integration' )] Param ( [Parameter( Mandatory=$false, HelpMessage = 'If turned on, the CmdLet will emit the current default domain' )] [switch]$showDefaultDomain, [Parameter( Mandatory=$false, HelpMessage = 'If turned on, the CmdLet will try to connect to Exchange Online is disconnected' )] [switch]$Connect ) [bool]$isConnected = $false Write-Verbose "Check if module ExchangeOnlineManagement is imported" if(!(Get-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue)) { Write-Warning "ExchangeOnlineManagement module not yet imported, importing ..." if(!(Import-Module ExchangeOnlineManagement -PassThru -ErrorAction SilentlyContinue)) {throw [System.Exception] "ExchangeOnlineManagement module does not seem to be installed! Use 'Install-Module ExchangeOnlineManagement' to install.'"} } else { $ExoConnInfo = if (Get-Connectioninformation) {(Get-ConnectionInformation)[-1]} if ($ExoConnInfo) { Write-Verbose "Connected to Exchange Online Tenant $($ExoConnInfo.TenantID)" if ((!($global:tenantAcceptedDomains)) -or ((Get-AcceptedDomain) -ne $tenantAcceptedDomains) ) { try { $global:tenantAcceptedDomains = Get-AcceptedDomain -Erroraction silentlycontinue } catch { Write-Error "Cannot detect accepted domains, maybe disconnected. Connect to Exchange Online and load the module again!" break } } [datetime]$TokenExpiryTimeLocal = $ExoConnInfo.TokenExpiryTimeUTC.Datetime.ToLocalTime() $delta = New-TimeSpan -Start (Get-Date) -End $TokenExpiryTimeLocal $ticks = $delta.Ticks if ($ticks -like '-*') # Token expired { $isconnected = $false Write-Warning "You're not actively connected to your Exchange Online organization. TOKEN is EXPIRED" if(($InteractiveSession) -and ($Connect))# defined in public/Functions.ps1 { try { # throws an exception if authentication fails Write-Verbose "Reconnecting to Exchange Online" Connect-ExchangeOnline -SkipLoadingFormatData $isConnected = $true } catch { throw [System.Exception] "Could not connect to Exchange Online, please retry."} } else { $isConnected = $false } } else # Valid connection { $tokenLifeTime = [math]::Round($delta.TotalHours) Write-verbose "Active session token exipry time is $TokenExpiryTimeLocal (roughly $tokenLifeTime hours)" $tmpModuleName = Split-Path -Path $ExoConnInfo.ModuleName -Leaf Write-verbose "Active session Module name is $tmpModuleName" $isConnected = $true [string] $Script:ExODefaultDomain = Get-AcceptedDomain | Where-Object{$_.Default} | Select-Object -ExpandProperty DomainName -First 1 if ($showDefaultDomain) {"$Script:ExoDefaultdomain"} } } else # No Connection { if(($InteractiveSession) -and ($connect)) # defined in public/Functions.ps1 { try { # throws an exception if authentication fails Write-Verbose "Connecting to Exchange Online" Connect-ExchangeOnline -SkipLoadingFormatData } catch { throw [System.Exception] "Could not connect to Exchange Online, please retry."} } else { $isConnected = $false } } } return $isConnected } <# .SYNOPSIS Validates if a given strig (domainname) is the tenant default maildomain .DESCRIPTION It takes the string and prooves if the domain is a member of the tenant. If this fails, the function returns an error and stops. If the domain is a tenant-member it returns true/false if its the domain default. .NOTES - none - .LINK - none - .EXAMPLE Confirm-SC365TenantDefaultDomain -Domain 'contoso.eu' Returns either an error if the domain is NOT in the tenant, of true or false. #> function Confirm-SC365TenantDefaultDomain { param ( [CmdLetBinding()] [Parameter(Mandatory = $true)] [String]$ValidationDomain ) begin { $TenantDefaultDomain = $TenantAcceptedDomains.Where({$_.Default -eq $true}).DomainName } process { If (!($TenantAcceptedDomains.DomainName -contains $ValidationDomain)) { Write-Error "$ValidationDomain is not member of the connected tenant. Retry using only tenant-domains (Use CmdLet: Get-AcceptedDomain)" break } else { if ($Validationdomain -eq $TenantDefaultDomain) { return $true } else { return $false } } } end { } } <# .SYNOPSIS Tracks Messages in Exchange Online connected with SEPPmail.cloud .DESCRIPTION Combines information from messagetrace and messagetrace details, to emit information how a specific message went throught Exchangeonline .NOTES - none - .LINK - none - .EXAMPLE Get-SC365Messagetrace -MessageId '123@somedomain.com' -RecipientAddress 'bob@contoso.com' #> function Get-SC365MessageTrace { [CmdLetBinding( HelpURI = 'https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md#setup-the-integration' )] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true)] [String]$MessageId, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true)] [Alias('RecipientAddress')] [String]$Recipient ) begin { $OldEaPreference = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' Write-Information "This CmdLet is still under development" -InformationAction Continue Write-Verbose "Collecting SEPPmail Connectors for Messagetrace-Detail analysis" try { if (!($ibc = Get-Inboundconnector -Identity '[SEPPmail*')) { Write-Error "Could not find SEPPmail Inbound-Connecor" } if (!($obc = Get-Outboundconnector -Identity '[SEPPmail*')) { Write-Error "Could not find SEPPmail Outbound-Connecor" } } catch { Write-Error "Could not detect SEPPmail Connectors, aborting" break } $TenantAcceptedDomains = Get-AcceptedDomain } process { Write-Verbose "Retrieving initial Message-Trace id MessageID $MessageId for recipient $Recipient" Write-Progress -Activity "Loading message data" -Status "MessageTrace" -PercentComplete 0 -CurrentOperation "Start" $PlainMessageID = $MessageId.Trim('<','>') Write-Verbose "Formatting Parameterinput Messageid:$MessageId - adding < and > at beginning and end to filter property" if (!($MessageId.StartsWith('<'))) {$MessageId = "<" + $MessageId} if (!($MessageId.EndsWith('>'))) {$MessageId = $MessageId + ">"} Write-Verbose "MessageID after formatting is now $MessageId" Write-Progress -Activity "Loading message data" -Status "MessageTrace" -PercentComplete 40 -CurrentOperation "Messages loaded" $MessageTrace = Get-MessageTrace -MessageId $PlainMessageId -RecipientAddress $Recipient if ($MessageTrace.count -eq 1) { Write-Verbose "Try to find modified encrypted message" $EncMessageTrace = Get-MessageTrace -RecipientAddress $Recipient | Where-Object {($_.MessageId -like '*' + $PlainMessageId + '>')} Write-verbose "Found message with modified MessageID $Messagetrace.MessageID" if ($EncMessageTrace.count -eq 2) { $MessageTrace = Get-MessageTrace -RecipientAddress $Recipient | Where-Object {($_.MessageId -like '*' + $PlainMessageId + '>')} } } if (!($MessageTrace)){ Write-Error "Could not find Message with ID $MessageID and recipient $recipient. Look for typos." Write-Error "Message too young ? Try Get-Messagetrace" Write-Error "Message too old ? Try Search-MessageTrackingReport " break } try { Write-verbose "Test Maildirection, based on the fact that the $Recipient is part of TenantDomains" If (($TenantAcceptedDomains.DomainName).Contains(($Recipient -Split '@')[-1])) { $MailDirection = 'InBound' } else { $MailDirection = 'OutBound' } } catch { Write-Error "Could not detech mail-direction of recipient-address $recipient. Check for typos and see error below." $error[0] break } Write-Verbose "Crafting basic MessageTraceInfo" $OutPutObject = [PSCustomObject][ordered]@{ Subject = if ($MessageTrace.count -eq 1) {$MessageTrace.Subject} else {$MessageTrace[0].Subject} Size = if ($MessageTrace.count -eq 1) {Convertto-SC365Numberformat -rawnumber $($MessageTrace.Size)} else {Convertto-SC365Numberformat -rawnumber $($MessageTrace[0].Size)} SenderAddresses = if ($MessageTrace.count -eq 1) {$MessageTrace.SenderAddress} else {$MessageTrace[0].SenderAddress} MailDirection = $MailDirection RoutingMode = if (($ibc.identity -eq "[SEPPmail.cloud] Inbound-Parallel") -or ($obc.identity -eq "[SEPPmail.cloud] Outbound-Parallel")) {'Parallel'} else {'Inline'} } Write-Verbose "Add MessageTraceId´s" foreach ($i in $Messagetrace) {Add-Member -InputObject $OutPutObject -MemberType NoteProperty -Name ('MessageTraceId' + ' Index'+ $I.Index) -Value $i.MessagetraceId} if ($MessageTrace.count -eq 1) { Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromIP -Value $MessageTrace.FromIP Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromDNS -Value (Resolve-DNS -query $MessageTrace.FromIP).Answers|Select-Object -ExpandProperty Address|Select-Object -ExpandProperty IPAddressToString if ($messagetrace.ToIp) {Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name ExternalToIP -Value $MessageTrace.ToIP} if ($messagetrace.ToIp) {Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name ExternalToDNS -Value (((Resolve-DNS -Query $MessageTrace.ToIP -QueryType PTR).Answers).PtrDomainName).Value} } else { if ($Messagetrace[0].FromIP) { Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromIP -Value $MessageTrace[0].FromIP Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromDNS -Value ((((Resolve-DNS -Query $MessageTrace[0].FromIP -QueryType PTR).Answers).PtrDomainName).Value) } else { Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromIP -Value '---empty---' Add-Member -InputObject $OutputObject -membertype NoteProperty -Name ExternalFromDNS -Value '---empty---' } if ($MessageTrace[0].ToIP) { Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name ExternalToIP -Value $MessageTrace[0].ToIP Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name ExternalToDNS -Value ((((Resolve-DNS -Query $MessageTrace[0].ToIP -QueryType PTR).Answers).PtrDomainName).Value) } else { Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name ExternalToIP -Value '---empty---' } } Add-Member -InputObject $OutPutObject -membertype NoteProperty -Name 'SplitLine' -Value "-------------------- MessageTrace DETAIL Info Starts Here --------------------" switch ($maildirection) { {(($_ -eq 'InBound') -and ($ibc.identity -match "\[SEPPmail\]") -and ($ibc.ConnectorType -eq 'OnPremises'))} { # Im Parallel Mode kommt die Mail 2x, einmal von externem Host und einmal von SEPpmail, Index 0 und 1 #$MessageTraceDetailExternal = Get-MessagetraceDetail -MessageTraceId $MessageTrace[1].MessageTraceId -Recipient $Recipient $MTDExtReceived = Get-MessagetraceDetail -MessageTraceId $MessageTrace[1].MessageTraceId -Recipient $Recipient -Event 'RECEIVE' $MTDExtExtSend = Get-MessagetraceDetail -MessageTraceId $MessageTrace[1].MessageTraceId -Recipient $Recipient | Where-Object event -like '*send*' #$MessageTraceDetailSEPPmail = Get-MessagetraceDetail -MessageTraceId $MessageTrace[0].MessageTraceId -Recipient $Recipient $MTDSEPPReceived = Get-MessagetraceDetail -MessageTraceId $MessageTrace[0].MessageTraceId -Recipient $Recipient -Event 'RECEIVE' $MTDSEPPDelivered = Get-MessagetraceDetail -MessageTraceId $MessageTrace[0].MessageTraceId -Recipient $Recipient -Event 'DELIVER' Write-Verbose "Crafting Inbound Connector Name" try { $ibcName = (($MTDSEPPReceived.Data).Split(';') | Select-String 'S:InboundConnectorData=Name').ToString().Split('=')[-1] } catch { $ibcName = '--- E-Mail did not go over SEPPmail Connector ---' } Write-Verbose "Preparing Output (Receive)Inbound-Parallel" $Outputobject | Add-Member -MemberType NoteProperty -Name ExternalReceivedTime -Value $messageTrace[1].Received $Outputobject | Add-Member -MemberType NoteProperty -Name ExternalReceivedSize -Value $messageTrace[1].Size $Outputobject | Add-Member -MemberType NoteProperty -Name FromExternalSendToIP -Value $messageTrace[1].ToIP $Outputobject | Add-Member -MemberType NoteProperty -Name FromExternalSendToDNS -Value ((((Resolve-DNS -Query $MessageTrace[1].ToIP -QueryType PTR).Answers).PtrDomainName).Value) $Outputobject | Add-Member -MemberType NoteProperty -Name ExtMessageTraceId -Value $MessageTrace[1].MessageTraceId.Guid $Outputobject | Add-Member -MemberType NoteProperty -Name SEPPMessageTraceId -Value $MessageTrace[0].MessageTraceId.Guid $Outputobject | Add-Member -MemberType NoteProperty -Name 'FullTransportTime(s)' -Value (New-TimeSpan -Start $MTDExtReceived.Date -End $MTDSEPPDelivered.Date).Seconds $Outputobject | Add-Member -MemberType NoteProperty -Name 'ExoTransportTime(s)' -Value (New-TimeSpan -Start $MTDExtReceived.Date -End $MTDExtExtSend.Date).Seconds $Outputobject | Add-Member -MemberType NoteProperty -Name 'SEPPTransportTime(s)' -Value (New-TimeSpan -Start $MTDSEPPReceived.Date -End $MTDSEPPDelivered.Date).Seconds #$Outputobject | Add-Member -MemberType NoteProperty -Name SubmitDetail -Value $MTDSEPPDelivered.Detail # Boring $Outputobject | Add-Member -MemberType NoteProperty -Name ExtSendDetail -Value $MTDExtExtSend.Detail $Outputobject | Add-Member -MemberType NoteProperty -Name InboundConnectorName -Value $ibcName } {(($_ -eq 'InBound') -and ($ibc.identity -match "\[SEPPmail\]") -and ($ibc.ConnectorType -eq 'Partner'))} { $MessageTraceDetail = Get-MessagetraceDetail -MessageTraceId $MessageTrace.MessageTraceId -Recipient $Recipient #$MTDReceived = $MessageTraceDetail|where-object {($_.Event -eq 'Received') -or ($_.Event -eq 'Empfangen')} $MTDReceived = Get-MessagetraceDetail -MessageTraceId $MessageTrace.MessageTraceId -Recipient $Recipient -event 'received' #$MTDDelivered = $MessageTraceDetail|where-object {($_.Event -eq 'Delivered') -or ($_.Event -eq 'Zustellen')} Write-Verbose "Crafting Inbound Connector Name" try { $ibcName = (($MTDReceived.Data).Split(';')|select-string 'S:InboundConnectorData=Name').ToString().Split('=')[-1] } catch { $ibcName = '--- E-Mail did not go over SEPPmail Connector ---' } Write-Verbose "Preparing Output (Receive)Inbound-Inline" $Outputobject | Add-Member -MemberType NoteProperty -Name ExternalReceivedTime -Value $messageTrace.Received #$Outputobject | Add-Member -MemberType NoteProperty -Name DeliveredDetail -Value $MTDDelivered.Detail # Boring Info $Outputobject | Add-Member -MemberType NoteProperty -Name ReceivedDetail -Value $MTDReceived.Detail if ($MTReceived) { $Outputobject | Add-Member -MemberType NoteProperty -Name 'ExoTransportTime (s)' -Value (New-TimeSpan -Start $MTReceived.Date -End $MTDelivered.Date).Seconds } else { $outPutObject | Add-Member -MemberType NoteProperty -Name 'ExoTransportTime (s)' -Value '---cannot determine transporttime in inline mode---' } $Outputobject | Add-Member -MemberType NoteProperty -Name InboundConnectorName -Value $ibcName } {(($_ -eq 'OutBound') -and ($obc.identity -match "\[SEPPmail\]") -and ($ibc.ConnectorType -eq 'OnPremises'))} { # We take one of 2 Send/Receive Messagetraces from SEPPmail and get the details # Now this one has 3 Parts. 0= Recieve from Mailboxhost, 1 = SumbitMessage (Exo internal), 2 = Send to SEPPmail $MTDSEPPReceive = Get-MessagetraceDetail -MessageTraceId $MessageTrace[1].MessageTraceId -Recipient $Recipient -Event 'receive' $MTDSEPPExtSend = Get-MessagetraceDetail -MessageTraceId $MessageTrace[1].MessageTraceId -Recipient $Recipient |where-object Event -like '*SEND*' # $MTDSEPPSubmit = $MessageTraceDetailSEPPmail[1] Not interesting for us $MTDExtReceive = Get-MessagetraceDetail -MessageTraceId $MessageTrace[0].MessageTraceId -Recipient $Recipient -Event 'receive' $MTDExtExtSend = Get-MessagetraceDetail -MessageTraceId $MessageTrace[0].MessageTraceId -Recipient $Recipient |where-object Event -like '*SEND*' try { $obcName = (((($MTDSEPPExtSend.Data -Split '<') -replace ('>','')) -split (';') | select-String 'S:Microsoft.Exchange.Hygiene.TenantOutboundConnectorCustomData').ToString()).Split('=')[-1] }catch { $obcName = "--- E-Mail did not go via a SEPPmail Connector ---" } $Outputobject | Add-Member -MemberType NoteProperty -Name FromExternalSendToIP -Value $messageTrace[1].ToIP $Outputobject | Add-Member -MemberType NoteProperty -Name FromExternalSendToDNS -Value ((((Resolve-DNS -Query $MessageTrace[1].ToIP -QueryType PTR).Answers).PtrDomainName).Value) $Outputobject | Add-Member -MemberType NoteProperty -Name SEPPmailReceivedFromIP -Value $messageTrace[1].FromIP try { $Outputobject | Add-Member -MemberType NoteProperty -Name SEPPmailReceivedFromDNS -Value ((((Resolve-DNS -Query $MessageTrace[1].FromIP -QueryType PTR).Answers).PtrDomainName).Value) } catch { Write-Information "Cannot Resolve $($messageTrace[1].FromIP)" -InformationAction Continue } $Outputobject | Add-Member -MemberType NoteProperty -Name 'ExoTransPortTime(s)' -Value (New-TimeSpan -Start $MTDExtReceive.Date -End $MTDExtExtSend.Date).Seconds $Outputobject | Add-Member -MemberType NoteProperty -Name 'SEPPmailTransPortTime(s)' -Value (New-TimeSpan -Start $MTDSEPPReceive.Date -End $MTDSEPPExtSend.Date).Seconds $Outputobject | Add-Member -MemberType NoteProperty -Name 'FullTransPortTime(s)' -Value (New-TimeSpan -Start $MTDSEPPReceive.Date -End $MTDExtExtSend.Date).Seconds $Outputobject | Add-Member -MemberType NoteProperty -Name SEPPReceiveDetail -Value $MTDSEPPReceive.Detail #$Outputobject | Add-Member -MemberType NoteProperty -Name SEPPSubmitDetail -Value $MTDSEPPSubmit.Detail # Boring $Outputobject | Add-Member -MemberType NoteProperty -Name SEPPSendExtDetail -Value $MTDSEPPExtSend.Detail $Outputobject | Add-Member -MemberType NoteProperty -Name ExtReceiveDetail -Value $MTDExtReceive.Detail $Outputobject | Add-Member -MemberType NoteProperty -Name ExtSendDetail -Value $MTDExtExtSend.Detail $Outputobject | Add-Member -MemberType NoteProperty -Name OutboundConnectorName -Value $obcName $Outputobject | Add-Member -MemberType NoteProperty -Name ExternalSendLatency -Value (((($MTDExtExtSend.Data -Split '<') -replace ('>','')) -split (';') | select-String 'S:ExternalSendLatency').ToString()).Split('=')[-1] } {(($_ -eq 'OutBound') -and ($obc.identity -match "\[SEPPmail\]") -and ($ibc.ConnectorType -eq 'Partner'))} { Write-Progress -Activity "Loading message data" -Status "MessageTrace" -PercentComplete 40 -CurrentOperation "Get-Messagetrace" $MessageTraceDetail = Get-MessagetraceDetail -MessageTraceId $MessageTrace.MessageTraceId -Recipient $Recipient # 1 = Empfangen/Receive $MTDReceive = $MessageTraceDetail|Where-Object {(($_.Event -eq 'Empfangen') -or ($_.Event -eq 'Receive'))} # 2 = Übermitteln/Submit # $MTDSubmit = $MessageTraceDetail|Where-Object {(($_.Event -eq 'Übermitteln') -or ($_.Event -eq 'Submit'))} # 3 = Send external/Extern senden $MTDExtSend = $MessageTraceDetail|Where-Object {(($_.Event -eq 'Send external') -or ($_.Event -eq 'Extern senden'))} Write-Progress -Activity "Loading message data" -Status "MessageTrace" -PercentComplete 80 -CurrentOperation "Preparing Output" Write-Verbose "Crafting Oubound Connector Name" try { $obcName = (((($MTDExtSend.Data -Split '<') -replace ('>','')) -split (';') | select-String 'S:Microsoft.Exchange.Hygiene.TenantOutboundConnectorCustomData').ToString()).Split('=')[-1] } catch { $obcName = '--- E-Mail did not go over SEPPmail Connector ---' } Write-verbose "Adding Specific Outbound-Inline Data to output" if (($MTDReceive.Date) -and ($MTDExtSend.Date)) {$Outputobject | Add-Member -MemberType NoteProperty -Name 'ExoInternalTransportTime(s)' -Value (New-TimeSpan -Start $MTDReceive.Date -End $MTDExtSend.Date).Seconds} $Outputobject | Add-Member -MemberType NoteProperty -Name ReceiveDetail -Value $MTDReceive.Detail #$Outputobject | Add-Member -MemberType NoteProperty -Name SubmitDetail -Value $MTDSubmit.Detail # Keine Relevante Info $Outputobject | Add-Member -MemberType NoteProperty -Name ExtSendDetail -Value $MTDExtSend.Detail $Outputobject | Add-Member -MemberType NoteProperty -Name OutboundConnectorName -Value $obcName if ($MTDExtSend) {$Outputobject | Add-Member -MemberType NoteProperty -Name ExternalSendLatency -Value (((($MTDExtSend.Data -Split '<') -replace ('>','')) -split (';') | select-String 'S:ExternalSendLatency').ToString()).Split('=')[-1]} Write-Progress -Activity "Loading message data" -Status "StatusMessage" -PercentComplete 100 -CurrentOperation "Done" } Default { $OutputObject | Add-Member -MemberType NoteProperty -Name "SEPPmail Integration" -Value 'none found' Write-Verbose "E-Mail direction was $_, could not detect SEPPmail" } } } end { return $OutPutObject $ErrorActionPreference = $OldEaPreference } } <# .SYNOPSIS Read Office/Microsoft365 Azure TenantID .DESCRIPTION Every Exchange Online is part of some sort of Microsoft Subscription and each subscription has an Azure Active Directory included. We need the TenantId to identify managed domains in seppmail.cloud .EXAMPLE PS C:\> Get-SC365TenantID -maildomain 'contoso.de' Explanation of what the example does .INPUTS Maildomain as string (mandatory) .OUTPUTS TenantID (GUID) as string .NOTES See https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md for more #> Function Show-SC365Tenant { [CmdLetBinding( HelpURI = 'https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md#setup-the-integration' )] param ( ) if(!(Test-SC365ConnectionStatus)) { throw [System.Exception] "You're not connected to Exchange Online - please connect prior to using this CmdLet" } else { Write-Verbose "Connected to Exchange Organization `"$Script:ExODefaultDomain`"" -InformationAction Continue try { $where = Get-OrganizationConfig $youare = [PSCustomobject]@{ 'Name' = $where.Name 'DisplayName' = $where.DisplayName 'DefaultMailboxRegion' = $where.DefaultMailboxRegion 'AllowedMailboxRegions' = $where.AllowedMailboxRegions 'ExchangeVersion' = $where.ExchangeVersion 'SendFromAliasEnabled' = $where.SendFromAliasEnabled 'IsTenantInGracePeriod' = $Where.IsTenantInGracePeriod } return $youare } catch { Write-Error $Error[0].Message } } } <# .SYNOPSIS Read DateTime of SEPPmail.cloud Setup .DESCRIPTION Reads the Creation Time of the SEPPmail.cloud Inbound Connector and emits dateTime of the moment the setup occured. .NOTES none .LINK none .EXAMPLE Get-SC365SetupTime Montag, 20. März 2023 10:56:04 .EXAMPLE Get-Sc365SetupTime -verbose Montag, 20. März 2023 10:56:04 VERBOSE: SEPPmail Cloud was created 13 days ago VERBOSE: Inbound Connector Comments writes install date is: 03/20/2023 10:56:04 #> Function Get-SC365SetupTime { [CmdLetBinding()] param () begin { if(!(Test-SC365ConnectionStatus)) { throw [System.Exception] "You're not connected to Exchange Online - please connect prior to using this CmdLet" break } } process { $ibc = Get-InboundConnector -Identity '[SEPPmail.Cloud]*' -ErrorAction SilentlyContinue| select-object -first 1 -ErrorAction SilentlyContinue if ($ibc) { $createDateTime = $ibc|Select-Object -ExpandProperty WhenCreated $days = (New-Timespan -Start (Get-Date $ibc.WhenCreated) -End (Get-Date)).Days Write-Output "Create time was $createDateTime ($days days ago)" $commentstime = ((($ibc.comment|select-String 'Created with') -split 'version').trim()[-1] -split 'on').trim()[-1] Write-Verbose "Inbound Connector SEPPmail Setup Comments writes install date is: $commentsTime" } else { Write-Warning "No inbond connector found, no or incomplete Setup" } } end{} } <# .SYNOPSIS Read Office/Microsoft365 Azure TenantID .DESCRIPTION Every Exchange Online is part of some sort of Microsoft Subscription and each subscription has an Azure Active Directory included. We need the TenantId to identify managed domains in seppmail.cloud .EXAMPLE PS C:\> Get-SC365TenantID -maildomain 'contoso.de' Explanation of what the example does .INPUTS Maildomain as string (mandatory) .OUTPUTS TenantID (GUID) as string .NOTES See https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md for more #> Function Get-SC365TenantID { [CmdLetBinding( HelpURI = 'https://github.com/seppmail/SEPPmail365cloud/blob/main/README.md#setup-the-integration' )] param ( [Parameter(Mandatory=$true)] [Alias('SEPPmailCloudDomain')] [string]$maildomain ) $uri = 'https://login.windows.net/' + $maildomain + '/.well-known/openid-configuration' $TenantId = (Invoke-WebRequest $uri| ConvertFrom-Json).token_endpoint.Split('/')[3] Return $tenantid } <# .SYNOPSIS Convert a DNS domain to IDNA Format .DESCRIPTION The CmdLet uses the System.Globalization.IdnMapping .NET Method to convert a DNS Domain with special characters like ä, ü, or ß to the IDNA format. .NOTES none .LINK none .EXAMPLE Get-Sc365SetupTime öbb.at xn--bb-eka.at #> function ConvertTo-SC365IDNA { [CmdLetBinding()] param ( [Parameter( Mandatory = $true, ValueFromPipeline = $true )] [string]$String ) $idn = [System.Globalization.IdnMapping]::new() return $idn.GetAscii($String) } <# .SYNOPSIS Convert a IDNA Formatted Domain to DNS .DESCRIPTION The CmdLet uses the System.Globalization.IdnMapping .NET Method to convert a IDNA Domain with special characters like ä, ü, or ß to the original DNS format. .NOTES none .LINK none .EXAMPLE Get-Sc365SetupTime xn--bb-eka.at öbb.at #> function ConvertFrom-SC365IDNA { [CmdLetBinding()] param ( [Parameter( Mandatory = $true, ValueFromPipeline = $true )] [string]$String ) $idn = [System.Globalization.IdnMapping]::new() return $idn.GetUnicode($String) } function Get-SC365ParallelModeConfig { [CmdletBinding()] param ( [Parameter( Mandatory = $false )] [string]$ExpectedArcTrustedSealers = "SEPPmail.cloud", # Expected ArcTrustedSealers value [Parameter( Mandatory = $false )] [string]$InboundConnectorIdentity = "[SEPPmail.cloud] Inbound-Parallel", # Expected InboundConnector identity pattern [Parameter( Mandatory = $false )] [bool]$ExpectedEFSkipIPs = $false, # Expected EFSkipIPs value [Parameter( Mandatory = $false )] [bool]$ExpectedEFSkipLastIP = $true, # Expected EFSkipLastIP value [Parameter( Mandatory = $false )] [string]$ExpectedContentFilterPolicyIdentity = "Default", # Expected Hosted Content Filter Policy Identity [Parameter( Mandatory = $false )] [string]$ExpectedMarkAsSpamSpfRecordHardFail = "Off" # Expected value for MarkAsSpamSpfRecordHardFail ) Begin { Write-Verbose "Initializing Exchange Online configuration check..." $results = @{} # Hashtable to store retrieved values $warnings = @() # Array to store warnings } Process { try { # Get current ARC configuration $arcConfig = Get-ArcConfig $results["ArcTrustedSealers"] = $arcConfig.ArcTrustedSealers if ($results["ArcTrustedSealers"] -notmatch "\b$ExpectedArcTrustedSealers\b") { $warnings += "ArcTrustedSealers does not contain '$ExpectedArcTrustedSealers', Found: '$($arcConfig.ArcTrustedSealers)'. This will be Updated with the Setup of the SEPPmail.cloud connectors automatic cally" } # Get current InboundConnector configuration (use Get-InboundConnector to retrieve details) if ($inboundConnector = Get-InboundConnector -Identity $InboundConnectorIdentity -ErrorAction SilentlyContinue) { $results["EFSkipIPs"] = $inboundConnector.EFSkipIPs $results["EFSkipLastIP"] = $inboundConnector.EFSkipLastIP if ($inboundConnector.EFSkipIPs -ne $ExpectedEFSkipIPs) { $warnings += "EFSkipIPs is not as expected! Expected: '$ExpectedEFSkipIPs', Found: '$($inboundConnector.EFSkipIPs)'" } if ($inboundConnector.EFSkipLastIP -ne $ExpectedEFSkipLastIP) { $warnings += "EFSkipLastIP is not as expected! Expected: '$ExpectedEFSkipLastIP', Found: '$($inboundConnector.EFSkipLastIP)'" } } else { Write-Warning "No Parallel Inbound Connector with Identity `'$InboundConnectorIdentity`' found" } # Get current Hosted Content Filter Policy configuration $contentFilterPolicy = Get-HostedContentFilterPolicy -Identity $ExpectedContentFilterPolicyIdentity $results["MarkAsSpamSpfRecordHardFail"] = $contentFilterPolicy.MarkAsSpamSpfRecordHardFail if ($contentFilterPolicy.MarkAsSpamSpfRecordHardFail -ne $ExpectedMarkAsSpamSpfRecordHardFail) { $warnings += "MarkAsSpamSpfRecordHardFail is not as expected! Expected: '$ExpectedMarkAsSpamSpfRecordHardFail', Found: '$($contentFilterPolicy.MarkAsSpamSpfRecordHardFail)'" } } catch { Write-Error "An error occurred while retrieving Exchange Online configuration. Error: $_" } } End { # Output the retrieved values Write-Output $results # Display warnings if any configuration does not match if ($warnings.Count -gt 0) { Write-Warning "The setup is not as required! See details below and use Set-SC365ParallelModeConfig to prepare your tenant" foreach ($warning in $warnings) { Write-Warning $warning } } else { Write-Output "`nThe setup MATCHES the expected configuration." } } } function Set-SC365ParallelModeConfig { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter( Mandatory = $false )] [string]$ContentFilterPolicyIdentity = "Default" # Default value for Content Filter Policy Identity ) Begin { Write-Verbose "Initializing Exchange Online configuration process..." $progressCounter = 0 $progressSteps = 1 } Process { try { # Task 1: Configure Hosted Content Filter Policy $progressCounter++ Write-Progress -Activity "Configuring Content Filter Policy" -Status "Step $progressCounter of $progressSteps" -PercentComplete (($progressCounter / $progressSteps) * 100) if ($PSCmdlet.ShouldProcess("Hosted Content Filter Policy", "Setting MarkAsSpamSpfRecordHardFail to Off for Identity $ContentFilterPolicyIdentity")) { Set-HostedContentFilterPolicy -Identity $ContentFilterPolicyIdentity -MarkAsSpamSpfRecordHardFail Off Write-Verbose "Content Filter Policy updated successfully." } } catch { Write-Error "An error occurred while configuring Exchange Online. Error: $_" } } End { Write-Progress -Activity "Configuring Exchange Online" -Status "Completed" -Completed } } Register-ArgumentCompleter -CommandName Get-SC365TenantId -ParameterName MailDomain -ScriptBlock $paramDomSB # SIG # Begin signature block # MIIVzAYJKoZIhvcNAQcCoIIVvTCCFbkCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCATldoeN+oDFM19 # uQhAhlOV5KXUsdPY9AlF3tTDU7I1QKCCEggwggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYaMIIEAqADAgECAhBiHW0M # UgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENv # ZGUgU2lnbmluZyBSb290IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5 # NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp # BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0G # CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjI # ztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NV # DgFigOMYzB2OKhdqfWGVoYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/3 # 6F09fy1tsB8je/RV0mIk8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05Zw # mRmTnAO5/arnY83jeNzhP06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm # +qxp4VqpB3MV/h53yl41aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUe # dyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz4 # 4MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBM # dlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQY # MBaAFDLrkpr/NZZILyhAQnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritU # pimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNV # HSUEDDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsG # A1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1 # YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsG # AQUFBzAChjpodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2Rl # U2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0 # aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURh # w1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0Zd # OaWTsyNyBBsMLHqafvIhrCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajj # cw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNc # WbWDRF/3sBp6fWXhz7DcML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalO # hOfCipnx8CaLZeVme5yELg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJs # zkyeiaerlphwoKx1uHRzNyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z7 # 6mKnzAfZxCl/3dq3dUNw4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5J # KdGvspbOrTfOXyXvmPL6E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHH # j95Ejza63zdrEcxWLDX6xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2 # Bev6SivBBOHY+uqiirZtg0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/ # L9Uo2bC5a4CH2RwwggZzMIIE26ADAgECAhAMcJlHeeRMvJV4PjhvyrrbMA0GCSqG # SIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0 # ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYw # HhcNMjMwMzIwMDAwMDAwWhcNMjYwMzE5MjM1OTU5WjBqMQswCQYDVQQGEwJERTEP # MA0GA1UECAwGQmF5ZXJuMSQwIgYDVQQKDBtTRVBQbWFpbCAtIERldXRzY2hsYW5k # IEdtYkgxJDAiBgNVBAMMG1NFUFBtYWlsIC0gRGV1dHNjaGxhbmQgR21iSDCCAiIw # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOapobQkNYCMP+Y33JcGo90Soe9Y # /WWojr4bKHbLNBzKqZ6cku2uCxhMF1Ln6xuI4ATdZvm4O7GqvplG9nF1ad5t2Lus # 5SLs45AYnODP4aqPbPU/2NGDRpfnceF+XhKeiYBwoIwrPZ04b8bfTpckj/tvenB9 # P8/9hAjWK97xv7+qsIz4lMMaCuWZgi8RlP6XVxsb+jYrHGA1UdHZEpunEFLaO9Ss # OPqatPAL2LNGs/JVuGdq9p47GKzn+vl+ANd5zZ/TIP1ifX76vorqZ9l9a5mzi/HG # vq43v2Cj3jrzIQ7uTbxtiLlPQUqkRzPRtiwTV80JdtRE+M+gTf7bT1CTvG2L3scf # YKFk7S80M7NydxV/qL+l8blGGageCzJ8svju2Mo4BB+ALWr+gBmCGqrM8YKy/wXR # tbvdEvBOLsATcHX0maw9xRCDRle2jO+ndYkTKZ92AMH6a/WdDfL0HrAWloWWSg62 # TxmJ/QiX54ILQv2Tlh1Al+pjGHN2evxS8i+XoWcUdHPIOoQd37yjnMjCN593wDzj # XCEuDABYw9BbvfSp29G/uiDGtjttDXzeMRdVCJFgULV9suBVP7yFh9pK/mVpz+aC # L2PvqiGYR41xRBKqwrfJEdoluRsqDy6KD985EdXkTvdIFKv0B7MfbcBCiGUBcm1r # fLAbs8Q2lqvqM4bxAgMBAAGjggGpMIIBpTAfBgNVHSMEGDAWgBQPKssghyi47G9I # ritUpimqF6TNDDAdBgNVHQ4EFgQUL96+KAGrvUgJnXwdVnA/uy+RlEcwDgYDVR0P # AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYD # VR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9z # ZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6 # Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYu # Y3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0 # aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYB # BQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMB4GA1UdEQQXMBWBE3N1cHBv # cnRAc2VwcG1haWwuY2gwDQYJKoZIhvcNAQEMBQADggGBAHnWpS4Jw/QiiLQi2EYv # THCtwKsj7O3G7wAN7wijSJcWF7iCx6AoCuCIgGdWiQuEZcv9pIUrXQ6jOSRHsDNX # SvIhCK9JakZJSseW/SCb1rvxZ4d0n2jm2SdkWf5j7+W+X4JHeCF9ZOw0ULpe5pFs # IGTh8bmTtUr3yA11yw4vHfXFwin7WbEoTLVKiL0ZUN0Qk+yBniPPSRRlUZIX8P4e # iXuw7lh9CMaS3HWRKkK89w//18PjUMxhTZJ6dszN2TAfwu1zxdG/RQqvxXUTTAxU # JrrCuvowtnDQ55yXMxkkSxWUwLxk76WvXwmohRdsavsGJJ9+yxj5JKOd+HIZ1fZ7 # oi0VhyOqFQAnjNbwR/TqPjRxZKjCNLXSM5YSMZKAhqrJssGLINZ2qDK/CEcVDkBS # 6Hke4jWMczny8nB8+ATJ84MB7tfSoXE7R0FMs1dinuvjVWIyg6klHigpeEiAaSaG # 5KF7vk+OlquA+x4ohPuWdtFxobOT2OgHQnK4bJitb9aDazGCAxowggMWAgEBMGgw # VDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UE # AxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNgIQDHCZR3nkTLyV # eD44b8q62zANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCC3D2S9XCJ1XXD3DofUsz3sdgWz # PovnG3y+83DtY+YMJTANBgkqhkiG9w0BAQEFAASCAgAJP5BnVEyE3FQRMwoIASbs # jP8IcbEcJTbKy66nBFY8Z+eqNxGxNDs4+G3OncX6Ltx0p/h41GRB+jz8MHNRoG+g # AU+Ntr9kcOCBz7bx/15m6Jwzh3ayng3szuR5H9i4QX2O9kyCjbOZuLaWGw30NByN # CZJnPA/X2oeToXSp4WEa74F9WUAhTprqNGFSkrPSXD/a6w+7lrx4bidqhViECyCc # gKjYcXHeBGBWIjqSzvFVGmrTYsj0FBPn1fydrPrFcdtB7oko6AkPyrzHPhs43r9l # dQ/+w12g6z9G/jqhs/jurcQgrQa+NTrglUyVa1cEleYPfMhBQeo86J18uEcIRHTL # HW89VRo0GEm+g0E3LnqAFCZlDiorJaV8MFDomUzflPuUO/aQyVZA9IBHIsEH+vCA # VWFE62ohbcy15doErqhDr73RLEWXNFaIwRuMQv3H+4OSMhoxK54v56wzNeW9buV1 # mQpElsqIzaARYfU4BwbcaOCVtXESTyxTMM2nmyRdeTiIE1Zq5x20yDh5NryL4gfS # YSL2/m529AO7mqpBuGxNQtXr2+rFWKdImH/BoilCuJNcxRIgSPw7NbkH/SEKgaEU # w1Ao78TJqSsRVbdCv2ZT7mvRRt+gWZo+GYximH+qJS4CovVSk4MeL48RngtYThvj # fSL7wZej6nHqMhfWOK7nQA== # SIG # End signature block |