Public/Get/Get-MessageTrace.ps1

function Get-MessageTrace { 
    <#
    .SYNOPSIS
    Retrieves Exchange Online message trace summaries and per-recipient details
    using the V2 cmdlets, with automatic date-range chunking and throttle-aware
    retries.
 
    .DESCRIPTION
    Executes a full message trace workflow against Exchange Online:
 
      1. VALIDATION Confirms that at least one filter (MessageId, Sender,
           Recipient, or Subject) is supplied and that StartDate < EndDate.
 
      2. CONNECTION Auto-connects to Exchange Online if the V2 cmdlets are not
           already visible in the session. Uses Connect-ExchangeOnlineAlways
           (internal wrapper) when available, otherwise falls back to the native
           Connect-ExchangeOnline.
 
      3. CHUNKED SUMMARY QUERY (Get-MessageTraceV2) EXO limits each
           Get-MessageTraceV2 request to a 10-day window and returns at most
           5,000 rows per call. This function automatically splits the requested
           date range into ≤10-day slices and uses the continuation-token
           pattern (StartingRecipientAddress + adjusted EndDate) to page through
           result sets larger than 5,000 rows.
 
      4. THROTTLE HANDLING All EXO calls are wrapped in an exponential-backoff
           retry loop (up to 5 attempts, doubling from 1 s to a 30 s cap) that
           retries on HTTP 429 / 503 / "Too many requests" / "temporarily
           unavailable" errors. A 200 ms inter-request pause is applied between
           continuation pages to stay within the tenant rate limit (~100
           requests / 5 min).
 
      5. DETAIL QUERY (Get-MessageTraceDetailV2) For every row in the summary,
           individual delivery-event details are fetched by MessageTraceId +
           RecipientAddress. Non-fatal per-recipient failures are logged as
           warnings and do not abort the run.
 
      6. EXPORT (optional) When settings.messageTrace.autoExport is true, or
           when -ExportFolder is supplied, both the summary view and the detail
           rows are passed to Export-MessageTraceResults. The -WhatIf flag is
           honoured; no files are written during a preview run.
 
      7. DISCONNECT Calls Invoke-DisconnectExchangeOnline at the end of every
           run.
 
    DATE HANDLING All timestamps returned by EXO are UTC. The local datetime
    values you pass to -StartDate / -EndDate are forwarded as-is; be mindful of
    timezone offset when specifying narrow windows.
 
    SUBJECT FILTER TYPE The SubjectFilterType parameter is auto-resolved at
    runtime against the actual enum exposed by the installed EXO module version.
    The resolution order is: caller-supplied value → config default → "Contains"
    → first available enum value. An invalid caller-supplied value falls back
    with a warning rather than a hard error.
 
    .PARAMETER MessageId
    Internet Message-ID of the email to trace (e.g.
    "<abc123@mail.contoso.com>"). Passed directly to Get-MessageTraceV2 as the
    MessageId filter. Can be combined with Sender/Recipient.
 
    .PARAMETER Sender
    SMTP address of the sending mailbox to filter on (e.g. "alice@contoso.com").
    Passed to Get-MessageTraceV2 as SenderAddress. Supports partial values only
    insofar as EXO's own filter supports them.
 
    .PARAMETER Recipient
    SMTP address of the recipient to filter on (e.g. "bob@contoso.com"). Passed
    to Get-MessageTraceV2 as RecipientAddress.
 
    .PARAMETER Subject
    Subject text to filter on. The matching mode is controlled by
    -SubjectFilterType (default: Contains). Wildcards are not supported; the
    value is passed verbatim to the EXO cmdlet.
 
    .PARAMETER SubjectFilterType
    Controls how the -Subject value is matched. Accepted values depend on the
    installed EXO module version (typically: Contains, StartsWith, Equals /
    Exact). When omitted, "Contains" is used if supported by the module,
    otherwise the first available enum value is selected automatically. Ignored
    when -Subject is not specified.
 
    .PARAMETER StartDate
    The inclusive start of the trace window. Accepts any [datetime] value.
    Defaults to now minus settings.messageTrace.defaultLookbackHours (config),
    falling back to 48 hours when the config value is absent or zero. EXO
    supports a maximum history of 90 days from today.
 
    .PARAMETER EndDate
    The inclusive end of the trace window. Defaults to the current date/time
    when omitted. Must be later than -StartDate.
 
    .PARAMETER ExportFolder
    Filesystem path to write CSV export files into. Overrides
    settings.messageTrace.defaultExportFolder (and the global
    paths.exportDirectory fallback) for this invocation. When omitted and
    settings.messageTrace.autoExport is false, no files are written.
 
    .INPUTS
    None. This function does not accept pipeline input.
 
    .OUTPUTS
    None. Results are written to the console via Write-Log, and optionally
    exported to CSV by Export-MessageTraceResults. No objects are returned to
    the pipeline.
 
    .EXAMPLE
    Get-MessageTrace -Sender "alice@contoso.com"
 
    Traces all mail from alice@contoso.com over the configured default lookback
    window (e.g. last 48 hours). Results are displayed in the console.
 
    .EXAMPLE
    Get-MessageTrace -Sender "alice@contoso.com" -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)
 
    Traces mail from alice over the last 7 days. The date range is automatically
    split into ≤10-day chunks (only one chunk needed here).
 
    .EXAMPLE
    Get-MessageTrace -Recipient "bob@contoso.com" -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date)
 
    30-day recipient trace. Three ≤10-day slices are issued automatically.
 
    .EXAMPLE
    Get-MessageTrace -Subject "Invoice" -SubjectFilterType StartsWith -StartDate (Get-Date).AddDays(-3)
 
    Traces messages whose subject starts with "Invoice" over the last 3 days.
 
    .EXAMPLE
    Get-MessageTrace -MessageId "<abc123@mail.contoso.com>" -Sender "alice@contoso.com"
 
    Combines MessageId and Sender filters to narrow results to a specific
    message from a known sender.
 
    .EXAMPLE
    Get-MessageTrace -Sender "alice@contoso.com" -ExportFolder "C:\Exports\Traces"
 
    Runs the trace and writes summary + detail CSV files to the specified
    folder, overriding the default export path from config.
 
    .EXAMPLE
    Get-MessageTrace -Sender "alice@contoso.com" -WhatIf
 
    Previews the trace run — EXO queries are still executed, but no CSV files
    are written and no disconnect is performed.
 
    .NOTES
    - Requires ExchangeOnlineManagement module v3.7.0 or later, which exposes
      Get-MessageTraceV2 and Get-MessageTraceDetailV2 after connecting.
    - EXO message trace history is limited to 90 days from today.
    - Each Get-MessageTraceV2 call covers at most 10 days and returns up to
      5,000 rows; both limits are handled automatically by this function.
    - All EXO timestamps are returned in UTC regardless of local timezone.
    - The 200 ms inter-page sleep and 5-attempt backoff are tuned for the
      default tenant throttle limit (~100 EXO requests per 5 minutes).
 
    .LINK
    Export-MessageTraceResults
 
    .LINK
    Connect-ExchangeOnlineAlways
 
    .LINK
    Invoke-DisconnectExchangeOnline
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter()][string]  $MessageId,
        [Parameter()][string]  $Sender,
        [Parameter()][string]  $Recipient,
        [Parameter()][string]  $Subject,
        [Parameter()][string]  $SubjectFilterType,
        [Parameter()][datetime]$StartDate,
        [Parameter()][datetime]$EndDate,
        [Parameter()][string]  $ExportFolder
    )

    # --- Config & defaults ---
    Initialize-TechToolboxRuntime

    $exo = $script:cfg.settings.exchangeOnline
    $mt = $script:cfg.settings.messageTrace

    # Make sure our in-house EXO module is imported
    Import-ExchangeOnlineModule  # v3.7.0+ exposes V2 cmdlets after connect

    # Lookback hours (safe default)
    $lookbackHours = [int]$mt.defaultLookbackHours
    if ($lookbackHours -le 0) { $lookbackHours = 48 }

    # Auto export flag
    $autoExport = [bool]$mt.autoExport

    # Resolve export folder default
    $defaultExport = $mt.defaultExportFolder
    if ([string]::IsNullOrWhiteSpace($defaultExport)) {
        $defaultExport = $script:cfg.paths.exportDirectory
    }

    # Resolve StartDate/EndDate defaults
    if (-not $StartDate) { $StartDate = (Get-Date).AddHours(-$lookbackHours) }
    if (-not $EndDate) { $EndDate = (Get-Date) }

    if ($StartDate -ge $EndDate) {
        Write-Log -Level Error -Message "StartDate must be earlier than EndDate."
        throw "Invalid date window."
    }

    # --- Validate search criteria ---
    if (-not $MessageId -and -not $Sender -and -not $Recipient -and -not $Subject) {
        Write-Log -Level Error -Message "You must specify at least one of: MessageId, Sender, Recipient, Subject."
        throw "At least one search filter is required."
    }

    # --- Ensure EXO connection and V2 availability ---
    # V2 cmdlets are only available after Connect-ExchangeOnline (they load into tmpEXO_*).
    # Import only the commands this wrapper needs so EXO's deprecated Get-MessageTrace
    # does not shadow the TechToolbox function after the first run.
    function Confirm-EXOConnected {
        if (-not (Get-Command -Name Get-MessageTraceV2 -ErrorAction SilentlyContinue)) {
            $connectCmd = Get-Command -Name Connect-ExchangeOnlineAlways -ErrorAction SilentlyContinue
            if ($connectCmd) {
                try {
                    $connectParams = @{}

                    if ($connectCmd.Parameters.ContainsKey('ShowBanner')) {
                        $connectParams.ShowBanner = $false
                    }

                    if ($connectCmd.Parameters.ContainsKey('ShowProgress')) {
                        $connectParams.ShowProgress = [bool]$exo.showProgress
                    }

                    if ($connectCmd.Parameters.ContainsKey('CommandName')) {
                        $connectParams.CommandName = @(
                            'Get-MessageTraceV2',
                            'Get-MessageTraceDetailV2',
                            'Disconnect-ExchangeOnline'
                        )
                    }

                    & $connectCmd @connectParams | Out-Null
                }
                catch {
                    Write-Log -Level Error -Message ("Failed to connect to Exchange Online: {0}" -f $_.Exception.Message)
                    throw
                }
            }
        }
    }
    Confirm-EXOConnected

    # Resolve cmdlets (they are Functions exported from tmpEXO_* after connect)
    try {
        $getTraceCmd = Get-Command -Name Get-MessageTraceV2       -ErrorAction Stop
        $getDetailCmd = Get-Command -Name Get-MessageTraceDetailV2 -ErrorAction Stop
    }
    catch {
        Write-Log -Level Error -Message ("Message Trace V2 cmdlets not available. Are you connected to EXO? {0}" -f $_.Exception.Message)
        throw
    }

    # --- Resolve SubjectFilterType with preference for user request, then
    # config default, then sensible fallback ---
    function Resolve-SubjectFilterType {
        param(
            [Parameter()][string]$Requested,
            [Parameter(Mandatory)][object]$TraceCommand,
            [Parameter()][string]$Default = 'Contains'
        )

        # If cmdlet doesn't expose SubjectFilterType, return $null (older module behavior)
        if (-not $TraceCommand.Parameters.ContainsKey('SubjectFilterType')) { return $null }

        $p = $TraceCommand.Parameters['SubjectFilterType']
        $allowed = @()

        # If it's an enum, capture allowed values
        try {
            if ($p.ParameterType -and $p.ParameterType.IsEnum) {
                $allowed = $p.ParameterType::GetEnumNames()
            }
        }
        catch { }

        # If user requested one, accept it if valid (case-insensitive)
        if ($Requested) {
            if (-not $allowed -or ($allowed -contains $Requested)) { return $Requested }

            $match = $allowed | Where-Object { $_.ToLower() -eq $Requested.ToLower() } | Select-Object -First 1
            if ($match) { return $match }

            Write-Log -Level Warn -Message ("Requested SubjectFilterType '{0}' not valid. Allowed: {1}. Falling back to '{2}'." -f $Requested, ($allowed -join ', '), $Default)
        }

        # Default preference (you said Contains is best)
        foreach ($candidate in @($Default, 'StartsWith', 'Equals', 'Exact')) {
            if (-not $allowed -or ($allowed -contains $candidate)) { return $candidate }
        }

        # If enum exists but none matched, pick first as last resort
        if ($allowed.Count -gt 0) { return $allowed[0] }

        return $null
    }

    # --- Helper: throttle-aware invoker with retries for transient 429/5xx ---
    function Invoke-WithBackoff {
        param([scriptblock]$Block)
        $delay = 1
        for ($i = 1; $i -le 5; $i++) {
            try { return & $Block }
            catch {
                $msg = $_.Exception.Message
                if ($msg -match 'Too many requests|429|throttle|temporarily unavailable|5\d{2}') {
                    Write-Log -Level Warn -Message ("Transient/throttle error (attempt {0}/5): {1} — sleeping {2}s" -f $i, $msg, $delay)
                    Start-Sleep -Seconds $delay
                    $delay = [Math]::Min($delay * 2, 30)
                    continue
                }
                throw
            }
        }
        throw "Exceeded retry attempts."
    }

    # --- Chunked V2 invoker (≤10-day slices + continuation when >5k rows) ---
    function Invoke-MessageTraceV2Chunked {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)][datetime]$StartDate,
            [Parameter(Mandatory)][datetime]$EndDate,
            [Parameter()][string] $MessageId,
            [Parameter()][string] $SenderAddress,
            [Parameter()][string] $RecipientAddress,
            [Parameter()][string] $Subject,
            [Parameter()][string] $SubjectFilterType,
            [Parameter()][int]    $ResultSize = 5000
        )
        # Docs: V2 supports 90 days history but only 10 days per request; up to
        # 5000 rows; times are returned as UTC. When result size is exceeded,
        # query subsequent data by using StartingRecipientAddress and EndDate
        # with the values from the previous result's Recipient address and
        # Received time.
        $sliceStart = $StartDate
        $endLimit = $EndDate
        $maxSpan = [TimeSpan]::FromDays(10)
        $results = New-Object System.Collections.Generic.List[object]

        while ($sliceStart -lt $endLimit) {
            $sliceEnd = $sliceStart.Add($maxSpan)
            if ($sliceEnd -gt $endLimit) { $sliceEnd = $endLimit }

            Write-Information ("[Trace] Querying slice {0:u} → {1:u}" -f $sliceStart.ToUniversalTime(), $sliceEnd.ToUniversalTime()) -InformationAction Continue

            $continuationRecipient = $null
            $continuationEndUtc = $sliceEnd

            do {
                $params = @{
                    StartDate   = $sliceStart
                    EndDate     = $continuationEndUtc
                    ResultSize  = $ResultSize
                    ErrorAction = 'Stop'
                }
                if ($MessageId) { $params.MessageId = $MessageId }
                if ($SenderAddress) { $params.SenderAddress = $SenderAddress }
                if ($RecipientAddress) { $params.RecipientAddress = $RecipientAddress }
                if ($Subject) {
                    $params.Subject = $Subject

                    # Newer EXO requires SubjectFilterType whenever Subject is specified
                    $resolvedSft = Resolve-SubjectFilterType -Requested $SubjectFilterType -TraceCommand $getTraceCmd -Default 'Contains'
                    if ($resolvedSft) {
                        $params.SubjectFilterType = $resolvedSft
                    }
                }

                if ($continuationRecipient) {
                    $params.StartingRecipientAddress = $continuationRecipient
                }

                $batch = Invoke-WithBackoff { & $getTraceCmd @params }
                $batchItems = @($batch)
                $batchCount = $batchItems.Count

                if ($batchCount -gt 0) {
                    $results.AddRange($batchItems)

                    # Continuation: use the oldest row's RecipientAddress and Received (UTC)
                    $last = $batchItems | Sort-Object Received -Descending | Select-Object -Last 1
                    $continuationRecipient = $last.RecipientAddress
                    $continuationEndUtc = $last.Received

                    # Pace to respect tenant throttling (100 req / 5 min)
                    Start-Sleep -Milliseconds 200
                }
                else {
                    $continuationRecipient = $null
                }

            } while ($batchCount -ge $ResultSize)

            $sliceStart = $sliceEnd
        }

        return $results
    }

    try {
        # --- Log filters (friendly) ---
        Write-Log -Level Info -Message "Message trace filters:"
        Write-Log -Level Info -Message (" MessageId : {0}" -f ($MessageId ?? '<none>'))
        Write-Log -Level Info -Message (" Sender : {0}" -f ($Sender ?? '<none>'))
        Write-Log -Level Info -Message (" Recipient : {0}" -f ($Recipient ?? '<none>'))
        Write-Log -Level Info -Message (" Subject : {0}" -f ($Subject ?? '<none>'))
        Write-Log -Level Info -Message (" Window : {0} → {1} (UTC shown by EXO)" -f $StartDate.ToString('u'), $EndDate.ToString('u'))

        # --- Execute (chunked) ---
        $summary = Invoke-MessageTraceV2Chunked `
            -StartDate         $StartDate `
            -EndDate           $EndDate `
            -MessageId         $MessageId `
            -SenderAddress     $Sender `
            -RecipientAddress  $Recipient `
            -Subject           $Subject `
            -SubjectFilterType $SubjectFilterType `
            -ResultSize        5000

        if (-not $summary -or $summary.Count -eq 0) {
            Write-Log -Level Warn -Message "No results found. Check filters, UTC vs. local time, and the 10-day-per-call limit."
            return
        }

        # Summary view (EXO returns UTC timestamps)
        $summaryView = $summary |
        Select-Object Received, SenderAddress, RecipientAddress, Subject, Status, MessageTraceId

        Write-Log -Level Ok   -Message ("Summary results ({0}):" -f $summaryView.Count)
        Write-Log -Level Info -Message ($summaryView | Sort-Object Received | Format-Table -AutoSize | Out-String)

        # --- Details ---
        Write-Log -Level Info -Message "Enumerating per-recipient details..."
        $detailsAll = New-Object System.Collections.Generic.List[object]

        foreach ($row in $summary) {
            $mtid = $row.MessageTraceId
            $rcpt = $row.RecipientAddress
            if (-not $mtid -or -not $rcpt) { continue }

            try {
                $details = Invoke-WithBackoff { & $getDetailCmd -MessageTraceId $mtid -RecipientAddress $rcpt -ErrorAction Stop }
                if ($details) {
                    $detailsView = @($details | Select-Object `
                    @{n = 'Recipient'; e = { $rcpt } },
                    @{n = 'MessageTraceId'; e = { $mtid } },
                    Date, Event, Detail)
                    $detailsAll.AddRange($detailsView)
                }
            }
            catch {
                Write-Log -Level Warn -Message ("Failed to get details for {0} / MTID {1}: {2}" -f $rcpt, $mtid, $_.Exception.Message)
            }
        }

        if ($detailsAll.Count -gt 0) {
            Write-Log -Level Ok   -Message ("Details ({0} rows):" -f $detailsAll.Count)
            Write-Log -Level Info -Message ($detailsAll | Format-Table -AutoSize | Out-String)
        }
        else {
            Write-Log -Level Warn -Message "No detail records returned."
        }

        # --- Export ---
        $shouldExport = $autoExport -or (-not [string]::IsNullOrWhiteSpace($ExportFolder))
        if ($shouldExport) {
            if ([string]::IsNullOrWhiteSpace($ExportFolder)) {
                $ExportFolder = $defaultExport
            }

            if ($PSCmdlet.ShouldProcess($ExportFolder, "Export message trace results")) {
                Export-MessageTraceResults `
                    -Summary $summaryView `
                    -Details $detailsAll `
                    -ExportFolder $ExportFolder `
                    -WhatIf:$WhatIfPreference `
                    -Confirm:$false
            }
        }
    }
    finally {
        [void](Invoke-DisconnectExchangeOnline)
    }
}
# SIG # Begin signature block
# MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCSfGSsOax1QGu2
# 8Z81ai27yKn2Bws8KM0IuLXi5S1mAKCCGEowggUMMIIC9KADAgECAhAR+U4xG7FH
# qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl
# IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD
# VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d
# nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi
# caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC
# Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs
# Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90
# Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6
# 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ
# TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs
# o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn
# gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi
# EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG
# MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW
# BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3
# QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8
# 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz
# 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ
# 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n
# 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j
# PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg
# WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT
# Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX
# 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea
# tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3
# JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+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
# twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg
# MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB
# cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p
# vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe
# HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd
# gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU
# jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR
# VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS
# LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV
# NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL
# 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd
# SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU
# eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/
# T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ
# E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r
# EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y
# 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx
# dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t
# y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy
# tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB
# YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud
# /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS
# uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP
# ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF
# ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV
# BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB
# MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL
# MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE
# aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg
# MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr
# V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8
# dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M
# rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ
# ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO
# nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n
# igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt
# K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1
# zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk
# 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW
# eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk
# prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G
# A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG
# fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB
# BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy
# NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB
# MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB
# MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL
# pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj
# g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q
# YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5
# bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG
# tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE
# suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U
# Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG
# 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV
# FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5
# t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs
# arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu
# ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3
# AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG
# AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBb14yJ8VZG
# qeI1A96AV2LK99aZbBF/xremug0iaeUTSjANBgkqhkiG9w0BAQEFAASCAgBKcB+x
# 1sOU/SDjKj3n+TsSxZv6XlkcM3OO5zIQ18aUIwZ+LKCUbI21srP6jsdXD+EeUmYl
# DSE/56yvlJMBl3m6CNPTbVYj2ZotwAQTnh/b3ehOBezb+/GeOPmWz1RNNq/IPeS7
# Klmi9h7+ZIdx93IQPN0dIKaJsOImMd3GLUA8C2DO7SKiQrReaaMlyEOjNF2dURhy
# bZqhOo8Yal8PHqNDcQE1R3cmlwcZztH2FB3VivgA3+tnceROtjU+T6+cs7Jstpi4
# 0SyLIYycGJea2G1gYRIu2WTDknXw5DzOuMgUY6Wtb2q1yFzbvPmA0dCvdcdEIseP
# B0kjiwwh/tBdlk6S+HCJaydMNlbIMvO8bwvL6PZH7/UQSrdBEyCakGvsnKdl8g+5
# DJdd3Nv2PzPQh1/PQrj6mdUkgyG2nBR5pwtzAckIu2GGwQg+w0KvnjlLBGmxu4U1
# 0JBEh17jQiPYXaeO5D1lkZgxN2vS4BaRI0AyRoBIq9/06WFaeczueC+uWDKHqWW3
# 3r64zSPNbWpj8Xgsclqub+2Z0XO3yxybGtcYHRA3yny3LLxNePKhWhh32aRJ8lEu
# GtB29IKNfuCOg4C+B70Whc4F5nq8Aikkh5nhVlnm/+e7peNDYHkHQ/QykyNcX1EU
# OWp2gM8LSGtmuk+i+U/WfVKfL1w6j442IzxwzqGCAyYwggMiBgkqhkiG9w0BCQYx
# ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg
# UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI
# AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
# BTEPFw0yNjA1MTIxMzQ4MTdaMC8GCSqGSIb3DQEJBDEiBCDkml0T+EO01FpEdI8P
# 5qvnAqB26SkqDnREE75FW2tNKTANBgkqhkiG9w0BAQEFAASCAgAjdOi4TE8lhPkf
# 1O7fSFh7p4vpWyIOSGAHI2R0afNqtACQLOjfZRwS474dTa3WQYiv9UZ3WDRI3GSt
# U5gk4PnGzsC4ziwIM441N44GfhJxiSQZEmbTINuxP+/pZjP6KPh/SbpIeFc3hUWk
# G3yX/9Xx9hzpx3IL902co78toj+M/JBMqgMIcafm36/uvaCFRU+yQxIZD2Qyi3b5
# wrh/RCV6mh1rQvOigAJ0aFrtkegdgiZeRyKuw9fAgos052urVclU6YOWlCWHJhtt
# 7dGYb8qrvmCWubEs7VwGx7c1tzC7fRXe1t3kQDdFTPMQmOzfHVGPPYR/ajZCOq0P
# kdL3A3eoRxeEEPdFqxDXA1SkzuwyX8hbRLE5KuS6oOQ0neC1XDMXqk/TsvDRpUiX
# Seq96IUj9vxaXw4iqkCQ0vKRVWi1h6YL8sRh7LDBjRMdp/zMN6X8xWkb04sCc5lq
# l7Xgasf+/t1yUFwGEPoATgOfWhrTu8E0M9uJk9k8nskgtFyJBxDN0E6pklMqWdJR
# 3WpSUpr92ytgKiGc5iuORKT5mYPVgT1y0p/PWEMetiMvhDqMSet52TDJY0MrXHh8
# pGtX3JnASNTUJkERt7aKAtuNU3XKEibt8BVDNl350itWgpcWxSKOV8mjS4RVwHqu
# YDOn+RTd7DWsZ35NN6SPxmeGeGRjcQ==
# SIG # End signature block