web: Format code files

This commit is contained in:
nemunaire 2026-01-24 19:18:26 +08:00
commit ac9b567025
22 changed files with 964 additions and 459 deletions

View file

@ -96,281 +96,442 @@
</h4>
</div>
<div class="list-group list-group-flush">
<!-- IPREV -->
{#if authentication.iprev}
<div class="list-group-item" id="authentication-iprev">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.iprev.result, true)} {getAuthResultClass(authentication.iprev.result, true)} me-2 fs-5"></i>
<div>
<strong>IP Reverse DNS</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.iprev.result, true)}">
{authentication.iprev.result}
</span>
{#if authentication.iprev.ip}
<div class="small">
<strong>IP Address:</strong>
<span class="text-muted">{authentication.iprev.ip}</span>
</div>
{/if}
{#if authentication.iprev.hostname}
<div class="small">
<strong>Hostname:</strong>
<span class="text-muted">{authentication.iprev.hostname}</span>
</div>
{/if}
{#if authentication.iprev.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.iprev.details}</pre>
{/if}
</div>
<!-- IPREV -->
{#if authentication.iprev}
<div class="list-group-item" id="authentication-iprev">
<div class="d-flex align-items-start">
<i
class="bi {getAuthResultIcon(
authentication.iprev.result,
true,
)} {getAuthResultClass(authentication.iprev.result, true)} me-2 fs-5"
></i>
<div>
<strong>IP Reverse DNS</strong>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.iprev.result,
true,
)}"
>
{authentication.iprev.result}
</span>
{#if authentication.iprev.ip}
<div class="small">
<strong>IP Address:</strong>
<span class="text-muted">{authentication.iprev.ip}</span>
</div>
{/if}
{#if authentication.iprev.hostname}
<div class="small">
<strong>Hostname:</strong>
<span class="text-muted">{authentication.iprev.hostname}</span>
</div>
{/if}
{#if authentication.iprev.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.iprev.details}</pre>
{/if}
</div>
</div>
{/if}
<!-- SPF (Required) -->
<div class="list-group-item">
<div class="d-flex align-items-start" id="authentication-spf">
{#if authentication.spf}
<i class="bi {getAuthResultIcon(authentication.spf.result, true)} {getAuthResultClass(authentication.spf.result, true)} me-2 fs-5"></i>
<div>
<strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.spf.result, true)}">
{authentication.spf.result}
</span>
{#if authentication.spf.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.spf.domain}</span>
</div>
{/if}
{#if authentication.spf.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.spf.details}</pre>
{/if}
</div>
{:else}
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">SPF record is required for proper email authentication</div>
</div>
{/if}
</div>
</div>
{/if}
<!-- DKIM (Required) -->
<div class="list-group-item" id="authentication-dkim">
{#if authentication.dkim && authentication.dkim.length > 0}
{#each authentication.dkim as dkim, i}
<div class="d-flex align-items-start" class:mt-3={i > 0}>
<i class="bi {getAuthResultIcon(dkim.result, true)} {getAuthResultClass(dkim.result, true)} me-2 fs-5"></i>
<div>
<strong>DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ''}</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(dkim.result, true)}">
{dkim.result}
</span>
{#if dkim.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{dkim.domain}</span>
</div>
{/if}
{#if dkim.selector}
<div class="small">
<strong>Selector:</strong>
<span class="text-muted">{dkim.selector}</span>
</div>
{/if}
{#if dkim.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{dkim.details}</pre>
{/if}
<!-- SPF (Required) -->
<div class="list-group-item">
<div class="d-flex align-items-start" id="authentication-spf">
{#if authentication.spf}
<i
class="bi {getAuthResultIcon(
authentication.spf.result,
true,
)} {getAuthResultClass(authentication.spf.result, true)} me-2 fs-5"
></i>
<div>
<strong>SPF</strong>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.spf.result,
true,
)}"
>
{authentication.spf.result}
</span>
{#if authentication.spf.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.spf.domain}</span>
</div>
</div>
{/each}
{/if}
{#if authentication.spf.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.spf.details}</pre>
{/if}
</div>
{:else}
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>DKIM</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">DKIM signature is required for proper email authentication</div>
<i
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
'missing',
true,
)} me-2 fs-5"
></i>
<div>
<strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText("missing")}
</span>
<div class="text-muted small">
SPF record is required for proper email authentication
</div>
</div>
{/if}
</div>
</div>
<!-- X-Google-DKIM (Optional) -->
{#if authentication.x_google_dkim}
<div class="list-group-item" id="authentication-x-google-dkim">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.x_google_dkim.result, false)} {getAuthResultClass(authentication.x_google_dkim.result, false)} me-2 fs-5"></i>
<!-- DKIM (Required) -->
<div class="list-group-item" id="authentication-dkim">
{#if authentication.dkim && authentication.dkim.length > 0}
{#each authentication.dkim as dkim, i}
<div class="d-flex align-items-start" class:mt-3={i > 0}>
<i
class="bi {getAuthResultIcon(dkim.result, true)} {getAuthResultClass(
dkim.result,
true,
)} me-2 fs-5"
></i>
<div>
<strong>X-Google-DKIM</strong>
<i class="bi bi-info-circle text-muted ms-1" title="Google's internal DKIM signature for messages routed through Gmail infrastructure"></i>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.x_google_dkim.result, false)}">
{authentication.x_google_dkim.result}
<strong>DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ""}</strong
>
<span
class="text-uppercase ms-2 {getAuthResultClass(dkim.result, true)}"
>
{dkim.result}
</span>
{#if authentication.x_google_dkim.domain}
{#if dkim.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.x_google_dkim.domain}</span>
<span class="text-muted">{dkim.domain}</span>
</div>
{/if}
{#if authentication.x_google_dkim.selector}
{#if dkim.selector}
<div class="small">
<strong>Selector:</strong>
<span class="text-muted">{authentication.x_google_dkim.selector}</span>
<span class="text-muted">{dkim.selector}</span>
</div>
{/if}
{#if authentication.x_google_dkim.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.x_google_dkim.details}</pre>
{#if dkim.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{dkim.details}</pre>
{/if}
</div>
</div>
</div>
{/if}
<!-- X-Aligned-From (Disabled) -->
{#if authentication.x_aligned_from}
<div class="list-group-item" id="authentication-x-aligned-from">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.x_aligned_from.result, false)} {getAuthResultClass(authentication.x_aligned_from.result, false)} me-2 fs-5"></i>
<div>
<strong>X-Aligned-From</strong>
<i class="bi bi-info-circle text-muted ms-1" title="Check that Mail From and Header From addresses are in alignment. See Domain Alignment section."></i>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.x_aligned_from.result, false)}">
{authentication.x_aligned_from.result}
</span>
{#if authentication.x_aligned_from.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.x_aligned_from.domain}</span>
</div>
{/if}
{#if authentication.x_aligned_from.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.x_aligned_from.details}</pre>
{/if}
</div>
</div>
</div>
{/if}
<!-- DMARC (Required) -->
<div class="list-group-item" id="authentication-dmarc">
{/each}
{:else}
<div class="d-flex align-items-start">
{#if authentication.dmarc}
<i class="bi {getAuthResultIcon(authentication.dmarc.result, true)} {getAuthResultClass(authentication.dmarc.result, true)} me-2 fs-5"></i>
<div>
<strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dmarc.result, true)}">
{authentication.dmarc.result}
</span>
{#if authentication.dmarc.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.dmarc.domain}</span>
</div>
{/if}
{#snippet DMARCPolicy(policy: string)}
<div class="small">
<strong>Policy:</strong>
<span
class="fw-bold"
class:text-success={policy == "reject"}
class:text-warning={policy == "quarantine"}
class:text-danger={policy == "none"}
class:bg-warning={policy != "none" && policy != "quarantine" && policy != "reject"}
>
{policy}
</span>
</div>
{/snippet}
{#if authentication.dmarc.result != "none"}
{#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
{@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{/if}
{/if}
{#if authentication.dmarc.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.dmarc.details}</pre>
{/if}
<i
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
'missing',
true,
)} me-2 fs-5"
></i>
<div>
<strong>DKIM</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText("missing")}
</span>
<div class="text-muted small">
DKIM signature is required for proper email authentication
</div>
{:else}
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">DMARC policy is required for proper email authentication</div>
</div>
{/if}
</div>
</div>
{/if}
</div>
<!-- X-Google-DKIM (Optional) -->
{#if authentication.x_google_dkim}
<div class="list-group-item" id="authentication-x-google-dkim">
<div class="d-flex align-items-start">
<i
class="bi {getAuthResultIcon(
authentication.x_google_dkim.result,
false,
)} {getAuthResultClass(
authentication.x_google_dkim.result,
false,
)} me-2 fs-5"
></i>
<div>
<strong>X-Google-DKIM</strong>
<i
class="bi bi-info-circle text-muted ms-1"
title="Google's internal DKIM signature for messages routed through Gmail infrastructure"
></i>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.x_google_dkim.result,
false,
)}"
>
{authentication.x_google_dkim.result}
</span>
{#if authentication.x_google_dkim.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.x_google_dkim.domain}</span
>
</div>
{/if}
{#if authentication.x_google_dkim.selector}
<div class="small">
<strong>Selector:</strong>
<span class="text-muted"
>{authentication.x_google_dkim.selector}</span
>
</div>
{/if}
{#if authentication.x_google_dkim.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.x_google_dkim
.details}</pre>
{/if}
</div>
</div>
</div>
{/if}
<!-- BIMI (Optional) -->
<div class="list-group-item" id="authentication-bimi">
<!-- X-Aligned-From (Disabled) -->
{#if authentication.x_aligned_from}
<div class="list-group-item" id="authentication-x-aligned-from">
<div class="d-flex align-items-start">
{#if authentication.bimi && authentication.bimi.result != "none"}
<i class="bi {getAuthResultIcon(authentication.bimi.result, false)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result, false)}">
{authentication.bimi.result}
</span>
{#if authentication.bimi.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.bimi.details}</pre>
{/if}
</div>
{:else if authentication.bimi && authentication.bimi.result == "none"}
<i class="bi bi-exclamation-circle-fill text-warning me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 text-warning">
NONE
</span>
<div class="text-muted small">Brand Indicators for Message Identification</div>
{#if authentication.bimi.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.bimi.details}</pre>
{/if}
</div>
{:else}
<i class="bi bi-info-circle text-muted me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 text-muted">
Optional
</span>
<div class="text-muted small">Brand Indicators for Message Identification (optional enhancement)</div>
</div>
{/if}
</div>
</div>
<!-- ARC (Optional) -->
{#if authentication.arc}
<div class="list-group-item" id="authentication-arc">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.arc.result, false)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"></i>
<div>
<strong>ARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result, false)}">
{authentication.arc.result}
</span>
{#if authentication.arc.chain_length}
<div class="text-muted small">Chain length: {authentication.arc.chain_length}</div>
{/if}
{#if authentication.arc.details}
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.arc.details}</pre>
{/if}
</div>
<i
class="bi {getAuthResultIcon(
authentication.x_aligned_from.result,
false,
)} {getAuthResultClass(
authentication.x_aligned_from.result,
false,
)} me-2 fs-5"
></i>
<div>
<strong>X-Aligned-From</strong>
<i
class="bi bi-info-circle text-muted ms-1"
title="Check that Mail From and Header From addresses are in alignment. See Domain Alignment section."
></i>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.x_aligned_from.result,
false,
)}"
>
{authentication.x_aligned_from.result}
</span>
{#if authentication.x_aligned_from.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted"
>{authentication.x_aligned_from.domain}</span
>
</div>
{/if}
{#if authentication.x_aligned_from.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.x_aligned_from
.details}</pre>
{/if}
</div>
</div>
{/if}
</div>
{/if}
<!-- DMARC (Required) -->
<div class="list-group-item" id="authentication-dmarc">
<div class="d-flex align-items-start">
{#if authentication.dmarc}
<i
class="bi {getAuthResultIcon(
authentication.dmarc.result,
true,
)} {getAuthResultClass(authentication.dmarc.result, true)} me-2 fs-5"
></i>
<div>
<strong>DMARC</strong>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.dmarc.result,
true,
)}"
>
{authentication.dmarc.result}
</span>
{#if authentication.dmarc.domain}
<div class="small">
<strong>Domain:</strong>
<span class="text-muted">{authentication.dmarc.domain}</span>
</div>
{/if}
{#snippet DMARCPolicy(policy: string)}
<div class="small">
<strong>Policy:</strong>
<span
class="fw-bold"
class:text-success={policy == "reject"}
class:text-warning={policy == "quarantine"}
class:text-danger={policy == "none"}
class:bg-warning={policy != "none" &&
policy != "quarantine" &&
policy != "reject"}
>
{policy}
</span>
</div>
{/snippet}
{#if authentication.dmarc.result != "none"}
{#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{@const policy = authentication.dmarc.details.replace(
/^.*policy.published-domain-policy=([^\s]+).*$/,
"$1",
)}
{@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{/if}
{/if}
{#if authentication.dmarc.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.dmarc.details}</pre>
{/if}
</div>
{:else}
<i
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
'missing',
true,
)} me-2 fs-5"
></i>
<div>
<strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText("missing")}
</span>
<div class="text-muted small">
DMARC policy is required for proper email authentication
</div>
</div>
{/if}
</div>
</div>
<!-- BIMI (Optional) -->
<div class="list-group-item" id="authentication-bimi">
<div class="d-flex align-items-start">
{#if authentication.bimi && authentication.bimi.result != "none"}
<i
class="bi {getAuthResultIcon(
authentication.bimi.result,
false,
)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"
></i>
<div>
<strong>BIMI</strong>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.bimi.result,
false,
)}"
>
{authentication.bimi.result}
</span>
{#if authentication.bimi.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.bimi.details}</pre>
{/if}
</div>
{:else if authentication.bimi && authentication.bimi.result == "none"}
<i class="bi bi-exclamation-circle-fill text-warning me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 text-warning"> NONE </span>
<div class="text-muted small">
Brand Indicators for Message Identification
</div>
{#if authentication.bimi.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.bimi.details}</pre>
{/if}
</div>
{:else}
<i class="bi bi-info-circle text-muted me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 text-muted"> Optional </span>
<div class="text-muted small">
Brand Indicators for Message Identification (optional enhancement)
</div>
</div>
{/if}
</div>
</div>
<!-- ARC (Optional) -->
{#if authentication.arc}
<div class="list-group-item" id="authentication-arc">
<div class="d-flex align-items-start">
<i
class="bi {getAuthResultIcon(
authentication.arc.result,
false,
)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"
></i>
<div>
<strong>ARC</strong>
<span
class="text-uppercase ms-2 {getAuthResultClass(
authentication.arc.result,
false,
)}"
>
{authentication.arc.result}
</span>
{#if authentication.arc.chain_length}
<div class="text-muted small">
Chain length: {authentication.arc.chain_length}
</div>
{/if}
{#if authentication.arc.details}
<pre
class="p-2 mb-0 {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} text-muted small"
style="white-space: pre-wrap">{authentication.arc.details}</pre>
{/if}
</div>
</div>
</div>
{/if}
</div>
</div>

View file

@ -2,8 +2,8 @@
import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import { theme } from "$lib/stores/theme";
import GradeDisplay from "./GradeDisplay.svelte";
import EmailPathCard from "./EmailPathCard.svelte";
import GradeDisplay from "./GradeDisplay.svelte";
interface Props {
blacklists: Record<string, BlacklistCheck[]>;
@ -16,11 +16,7 @@
</script>
<div class="card shadow-sm" id="rbl-details">
<div
class="card-header"
class:bg-white={$theme === 'light'}
class:bg-dark={$theme !== 'light'}
>
<div class="card-header" class:bg-white={$theme === "light"} class:bg-dark={$theme !== "light"}>
<h4 class="mb-0 d-flex justify-content-between align-items-center">
<span>
<i class="bi bi-shield-exclamation me-2"></i>
@ -54,9 +50,19 @@
<tbody>
{#each checks as check}
<tr>
<td title={check.response || '-'}>
<span class="badge {check.listed ? 'bg-danger' : check.error ? 'bg-dark' : 'bg-success'}">
{check.error ? 'Error' : (check.listed ? 'Listed' : 'Clean')}
<td title={check.response || "-"}>
<span
class="badge {check.listed
? 'bg-danger'
: check.error
? 'bg-dark'
: 'bg-success'}"
>
{check.error
? "Error"
: check.listed
? "Listed"
: "Clean"}
</span>
</td>
<td><code>{check.rbl}</code></td>

View file

@ -36,16 +36,28 @@
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="bi {contentAnalysis.has_html ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
<i
class="bi {contentAnalysis.has_html
? 'bi-check-circle text-success'
: 'bi-x-circle text-muted'} me-2"
></i>
<span>HTML Part</span>
</div>
<div class="d-flex align-items-center mb-2">
<i class="bi {contentAnalysis.has_plaintext ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
<i
class="bi {contentAnalysis.has_plaintext
? 'bi-check-circle text-success'
: 'bi-x-circle text-muted'} me-2"
></i>
<span>Plaintext Part</span>
</div>
{#if typeof contentAnalysis.has_unsubscribe_link === 'boolean'}
{#if typeof contentAnalysis.has_unsubscribe_link === "boolean"}
<div class="d-flex align-items-center mb-2">
<i class="bi {contentAnalysis.has_unsubscribe_link ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'} me-2"></i>
<i
class="bi {contentAnalysis.has_unsubscribe_link
? 'bi-check-circle text-success'
: 'bi-x-circle text-warning'} me-2"
></i>
<span>Unsubscribe Link</span>
</div>
{/if}
@ -74,7 +86,14 @@
<div class="mt-3">
<h5>Content Issues</h5>
{#each contentAnalysis.html_issues as issue}
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
<div
class="alert alert-{issue.severity === 'critical' ||
issue.severity === 'high'
? 'danger'
: issue.severity === 'medium'
? 'warning'
: 'info'} py-2 px-3 mb-2"
>
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>{issue.type}</strong>
@ -118,11 +137,17 @@
{/if}
</td>
<td>
<span class="badge {link.status === 'valid' ? 'bg-success' : link.status === 'broken' ? 'bg-danger' : 'bg-warning'}">
<span
class="badge {link.status === 'valid'
? 'bg-success'
: link.status === 'broken'
? 'bg-danger'
: 'bg-warning'}"
>
{link.status}
</span>
</td>
<td>{link.http_code || '-'}</td>
<td>{link.http_code || "-"}</td>
</tr>
{/each}
</tbody>
@ -146,11 +171,11 @@
<tbody>
{#each contentAnalysis.images as image}
<tr>
<td><small class="text-break">{image.src || '-'}</small></td>
<td><small class="text-break">{image.src || "-"}</small></td>
<td>
{#if image.has_alt}
<i class="bi bi-check-circle text-success me-1"></i>
<small>{image.alt_text || 'Present'}</small>
<small>{image.alt_text || "Present"}</small>
{:else}
<i class="bi bi-x-circle text-warning me-1"></i>
<small class="text-muted">Missing</small>

View file

@ -1,15 +1,15 @@
<script lang="ts">
import type { DomainAlignment, DnsResults, ReceivedHop } from "$lib/api/types.gen";
import type { DnsResults, DomainAlignment, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import { theme } from "$lib/stores/theme";
import GradeDisplay from "./GradeDisplay.svelte";
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte";
import BimiRecordDisplay from "./BimiRecordDisplay.svelte";
import DkimRecordsDisplay from "./DkimRecordsDisplay.svelte";
import DmarcRecordDisplay from "./DmarcRecordDisplay.svelte";
import BimiRecordDisplay from "./BimiRecordDisplay.svelte";
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
import GradeDisplay from "./GradeDisplay.svelte";
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte";
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte";
interface Props {
domainAlignment?: DomainAlignment;
@ -20,7 +20,14 @@
domainOnly?: boolean; // If true, only shows domain-level DNS records (no PTR, no DKIM, simplified view)
}
let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain, domainOnly = false }: Props = $props();
let {
domainAlignment,
dnsResults,
dnsGrade,
dnsScore,
receivedChain,
domainOnly = false,
}: Props = $props();
// Extract sender IP from first hop
const senderIp = $derived(
@ -67,7 +74,10 @@
{#if receivedChain && receivedChain.length > 0}
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0 text-truncate">
Received from: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
Received from: <code
>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0]
.ip}])</code
>
</h4>
</div>
{/if}
@ -88,10 +98,13 @@
<div class="mb-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<h4 class="mb-0 text-truncate">
Return-Path Domain: <code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
Return-Path Domain:
<code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
</h4>
{#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain</span>
<span class="badge bg-danger ms-2">
<i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain
</span>
<small>
<i class="bi bi-chevron-right"></i>
<a href="#domain-alignment">See domain alignment</a>
@ -114,10 +127,13 @@
{/if}
<!-- SPF Records (for Return-Path Domain) -->
<SpfRecordsDisplay spfRecords={dnsResults.spf_records} dmarcRecord={dnsResults.dmarc_record} />
<SpfRecordsDisplay
spfRecords={dnsResults.spf_records}
dmarcRecord={dnsResults.dmarc_record}
/>
{#if !domainOnly}
<hr class="my-4">
<hr class="my-4" />
<!-- From Domain Section -->
<div class="mb-3 d-flex align-items-center gap-2">
@ -125,31 +141,34 @@
From Domain: <code>{dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path domain</span>
<span class="badge bg-danger ms-2">
<i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path
domain
</span>
{/if}
</div>
{/if}
<!-- MX Records for From Domain -->
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.from_mx_records}
title="Mail Exchange Records for From Domain"
description="These MX records handle replies to emails sent from this domain."
/>
{/if}
<!-- MX Records for From Domain -->
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.from_mx_records}
title="Mail Exchange Records for From Domain"
description="These MX records handle replies to emails sent from this domain."
/>
{/if}
{#if !domainOnly}
<!-- DKIM Records -->
<DkimRecordsDisplay dkimRecords={dnsResults.dkim_records} />
{/if}
<!-- DMARC Record -->
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
<!-- DMARC Record -->
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
<!-- BIMI Record -->
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
<!-- BIMI Record -->
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
{/if}
</div>
</div>

View file

@ -17,15 +17,25 @@
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<span class="badge bg-primary me-2">{receivedChain.length - i}</span>
{hop.reverse || '-'} {#if hop.ip}<span class="text-muted">({hop.ip})</span>{/if}{hop.by || 'Unknown'}
{hop.reverse || "-"}
{#if hop.ip}<span class="text-muted">({hop.ip})</span>{/if}{hop.by ||
"Unknown"}
</h6>
<small class="text-muted" title={hop.timestamp}>{hop.timestamp ? new Intl.DateTimeFormat('default', { dateStyle: 'long', 'timeStyle': 'short' }).format(new Date(hop.timestamp)) : '-'}</small>
<small class="text-muted" title={hop.timestamp}>
{hop.timestamp
? new Intl.DateTimeFormat("default", {
dateStyle: "long",
timeStyle: "short",
}).format(new Date(hop.timestamp))
: "-"}
</small>
</div>
{#if hop.with || hop.id}
<p class="mb-1 small d-flex gap-3">
{#if hop.with}
<span>
<span class="text-muted">Protocol:</span> <code>{hop.with}</code>
<span class="text-muted">Protocol:</span>
<code>{hop.with}</code>
</span>
{/if}
{#if hop.id}

View file

@ -44,10 +44,7 @@
}
</script>
<strong
class={getSizeClass(size)}
style="color: {getGradeColor(grade)}; font-weight: 700;"
>
<strong class={getSizeClass(size)} style="color: {getGradeColor(grade)}; font-weight: 700;">
{#if grade}
{grade}
{:else}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { AuthResult, DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
import type { DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import { theme } from "$lib/stores/theme";
import GradeDisplay from "./GradeDisplay.svelte";
@ -38,7 +38,14 @@
<div class="mb-3">
<h5>Issues</h5>
{#each headerAnalysis.issues as issue}
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
<div
class="alert alert-{issue.severity === 'critical' ||
issue.severity === 'high'
? 'danger'
: issue.severity === 'medium'
? 'warning'
: 'info'} py-2 px-3 mb-2"
>
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>{issue.header}</strong>
@ -58,24 +65,48 @@
{/if}
{#if headerAnalysis.domain_alignment}
{@const spfStrictAligned = headerAnalysis.domain_alignment.from_domain === headerAnalysis.domain_alignment.return_path_domain}
{@const spfRelaxedAligned = headerAnalysis.domain_alignment.from_org_domain === headerAnalysis.domain_alignment.return_path_org_domain}
{@const spfStrictAligned =
headerAnalysis.domain_alignment.from_domain ===
headerAnalysis.domain_alignment.return_path_domain}
{@const spfRelaxedAligned =
headerAnalysis.domain_alignment.from_org_domain ===
headerAnalysis.domain_alignment.return_path_org_domain}
<div class="card mb-3" id="domain-alignment">
<div class="card-header">
<h5 class="mb-0">
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill text-success' : headerAnalysis.domain_alignment.relaxed_aligned ? 'bi-check-circle text-info' : 'bi-x-circle-fill text-danger'}"></i>
<i
class="bi {headerAnalysis.domain_alignment.aligned
? 'bi-check-circle-fill text-success'
: headerAnalysis.domain_alignment.relaxed_aligned
? 'bi-check-circle text-info'
: 'bi-x-circle-fill text-danger'}"
></i>
Domain Alignment
</h5>
</div>
<div class="card-body">
<p class="card-text small text-muted">
Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path or DKIM signature). Proper alignment is crucial for DMARC compliance, regardless of the policy. It helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. Only one of the following lines needs to pass.
Domain alignment ensures that the visible "From" domain matches the domain
used for authentication (Return-Path or DKIM signature). Proper alignment is
crucial for DMARC compliance, regardless of the policy. It helps prevent
email spoofing by verifying that the sender domain is consistent across all
authentication layers. Only one of the following lines needs to pass.
</p>
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
<div class="mt-3">
{#each headerAnalysis.domain_alignment.issues as issue}
<div class="alert alert-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info' : 'warning'} py-2 small mb-2">
<i class="bi bi-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info-circle' : 'exclamation-triangle'} me-1"></i>
<div
class="alert alert-{headerAnalysis.domain_alignment
.relaxed_aligned
? 'info'
: 'warning'} py-2 small mb-2"
>
<i
class="bi bi-{headerAnalysis.domain_alignment
.relaxed_aligned
? 'info-circle'
: 'exclamation-triangle'} me-1"
></i>
{issue}
</div>
{/each}
@ -84,7 +115,10 @@
</div>
<div class="list-group list-group-flush">
<div class="list-group-item d-flex ps-0">
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
<div
class="d-flex align-items-center justify-content-center"
style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;"
>
SPF
</div>
<div class="flex-fill">
@ -92,9 +126,17 @@
<div class="col-md-3">
<small class="text-muted">Strict Alignment</small>
<div>
<span class="badge" class:bg-success={spfStrictAligned} class:bg-danger={!spfStrictAligned}>
<i class="bi {spfStrictAligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
<strong>{spfStrictAligned ? 'Pass' : 'Fail'}</strong>
<span
class="badge"
class:bg-success={spfStrictAligned}
class:bg-danger={!spfStrictAligned}
>
<i
class="bi {spfStrictAligned
? 'bi-check-circle-fill'
: 'bi-x-circle-fill'} me-1"
></i>
<strong>{spfStrictAligned ? "Pass" : "Fail"}</strong>
</span>
</div>
<div class="small text-muted mt-1">Exact domain match</div>
@ -102,38 +144,78 @@
<div class="col-md-3">
<small class="text-muted">Relaxed Alignment</small>
<div>
<span class="badge" class:bg-success={spfRelaxedAligned} class:bg-danger={!spfRelaxedAligned}>
<i class="bi {spfRelaxedAligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
<strong>{spfRelaxedAligned ? 'Pass' : 'Fail'}</strong>
<span
class="badge"
class:bg-success={spfRelaxedAligned}
class:bg-danger={!spfRelaxedAligned}
>
<i
class="bi {spfRelaxedAligned
? 'bi-check-circle-fill'
: 'bi-x-circle-fill'} me-1"
></i>
<strong>{spfRelaxedAligned ? "Pass" : "Fail"}</strong>
</span>
</div>
<div class="small text-muted mt-1">Organizational domain match</div>
<div class="small text-muted mt-1">
Organizational domain match
</div>
</div>
<div class="col-md-3">
<small class="text-muted">From Domain</small>
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
<div>
<code>
{headerAnalysis.domain_alignment.from_domain || "-"}
</code>
</div>
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
<div class="small text-muted mt-1">
Org:
<code>
{headerAnalysis.domain_alignment.from_org_domain}
</code>
</div>
{/if}
</div>
<div class="col-md-3">
<small class="text-muted">Return-Path Domain</small>
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
<div>
<code>
{headerAnalysis.domain_alignment.return_path_domain ||
"-"}
</code>
</div>
{#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain}
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.return_path_org_domain}</code></div>
<div class="small text-muted mt-1">
Org:
<code>
{headerAnalysis.domain_alignment
.return_path_org_domain}
</code>
</div>
{/if}
</div>
</div>
<!-- Alignment Information based on DMARC policy -->
{#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain}
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
{#if dmarcRecord.spf_alignment === 'strict'}
<div
class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment ===
'strict'
? 'alert-warning'
: 'alert-info'}"
>
{#if dmarcRecord.spf_alignment === "strict"}
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Strict SPF alignment required</strong> — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment.
<strong>Strict SPF alignment required</strong> — Your DMARC policy
requires exact domain match. The Return-Path domain must exactly
match the From domain for SPF to pass DMARC alignment.
{:else}
<i class="bi bi-info-circle me-1"></i>
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass.
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy
allows organizational domain matching. As long as both domains
share the same organizational domain (e.g., mail.example.com
and example.com), SPF alignment can pass.
{/if}
</div>
{/if}
@ -141,10 +223,16 @@
</div>
{#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain}
{@const dkim_aligned = dkim_domain.domain === headerAnalysis.domain_alignment.from_domain}
{@const dkim_relaxed_aligned = dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
{@const dkim_aligned =
dkim_domain.domain === headerAnalysis.domain_alignment.from_domain}
{@const dkim_relaxed_aligned =
dkim_domain.org_domain ===
headerAnalysis.domain_alignment.from_org_domain}
<div class="list-group-item d-flex ps-0">
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
<div
class="d-flex align-items-center justify-content-center"
style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;"
>
DKIM
</div>
<div class="flex-fill">
@ -153,35 +241,72 @@
<div class="col-md-3">
<small class="text-muted">Strict Alignment</small>
<div>
<span class="badge" class:bg-success={dkim_aligned} class:bg-danger={!dkim_aligned}>
<i class="bi {dkim_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
<strong>{dkim_aligned ? 'Pass' : 'Fail'}</strong>
<span
class="badge"
class:bg-success={dkim_aligned}
class:bg-danger={!dkim_aligned}
>
<i
class="bi {dkim_aligned
? 'bi-check-circle-fill'
: 'bi-x-circle-fill'} me-1"
></i>
<strong>{dkim_aligned ? "Pass" : "Fail"}</strong
>
</span>
</div>
<div class="small text-muted mt-1">Exact domain match</div>
<div class="small text-muted mt-1">
Exact domain match
</div>
</div>
<div class="col-md-3">
<small class="text-muted">Relaxed Alignment</small>
<div>
<span class="badge" class:bg-success={dkim_relaxed_aligned} class:bg-danger={!dkim_relaxed_aligned}>
<i class="bi {dkim_relaxed_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
<strong>{dkim_relaxed_aligned ? 'Pass' : 'Fail'}</strong>
<span
class="badge"
class:bg-success={dkim_relaxed_aligned}
class:bg-danger={!dkim_relaxed_aligned}
>
<i
class="bi {dkim_relaxed_aligned
? 'bi-check-circle-fill'
: 'bi-x-circle-fill'} me-1"
></i>
<strong
>{dkim_relaxed_aligned
? "Pass"
: "Fail"}</strong
>
</span>
</div>
<div class="small text-muted mt-1">Organizational domain match</div>
<div class="small text-muted mt-1">
Organizational domain match
</div>
</div>
<div class="col-md-3">
<small class="text-muted">From Domain</small>
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
<div>
<code
>{headerAnalysis.domain_alignment.from_domain ||
"-"}</code
>
</div>
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
<div class="small text-muted mt-1">
Org: <code
>{headerAnalysis.domain_alignment
.from_org_domain}</code
>
</div>
{/if}
</div>
<div class="col-md-3">
<small class="text-muted">Signature Domain</small>
<div><code>{dkim_domain.domain || '-'}</code></div>
<div><code>{dkim_domain.domain || "-"}</code></div>
{#if dkim_domain.domain !== dkim_domain.org_domain}
<div class="small text-muted mt-1">Org: <code>{dkim_domain.org_domain}</code></div>
<div class="small text-muted mt-1">
Org: <code>{dkim_domain.org_domain}</code>
</div>
{/if}
</div>
</div>
@ -189,13 +314,25 @@
<!-- Alignment Information based on DMARC policy -->
{#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain}
{#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.dkim_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
{#if dmarcRecord.dkim_alignment === 'strict'}
<div
class="alert mt-2 mb-0 small py-2 {dmarcRecord.dkim_alignment ===
'strict'
? 'alert-warning'
: 'alert-info'}"
>
{#if dmarcRecord.dkim_alignment === "strict"}
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Strict DKIM alignment required</strong> — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment.
<strong>Strict DKIM alignment required</strong>
Your DMARC policy requires exact domain match. The
DKIM signature domain must exactly match the From
domain for DKIM to pass DMARC alignment.
{:else}
<i class="bi bi-info-circle me-1"></i>
<strong>Relaxed DKIM alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass.
<strong>Relaxed DKIM alignment allowed</strong>
Your DMARC policy allows organizational domain matching.
As long as both domains share the same organizational
domain (e.g., mail.example.com and example.com),
DKIM alignment can pass.
{/if}
</div>
{/if}
@ -224,9 +361,9 @@
</thead>
<tbody>
{#each Object.entries(headerAnalysis.headers).sort((a, b) => {
const importanceOrder = { 'required': 0, 'recommended': 1, 'optional': 2, 'newsletter': 3 };
const aImportance = importanceOrder[a[1].importance || 'optional'];
const bImportance = importanceOrder[b[1].importance || 'optional'];
const importanceOrder = { required: 0, recommended: 1, optional: 2, newsletter: 3 };
const aImportance = importanceOrder[a[1].importance || "optional"];
const bImportance = importanceOrder[b[1].importance || "optional"];
return aImportance - bImportance;
}) as [name, check]}
<tr>
@ -235,23 +372,39 @@
</td>
<td>
{#if check.importance}
<small class="text-{check.importance === 'required' ? 'danger' : check.importance === 'recommended' ? 'warning' : 'secondary'}">
<small
class="text-{check.importance === 'required'
? 'danger'
: check.importance === 'recommended'
? 'warning'
: 'secondary'}"
>
{check.importance}
</small>
{/if}
</td>
<td>
<i class="bi {check.present ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
<i
class="bi {check.present
? 'bi-check-circle text-success'
: 'bi-x-circle text-danger'}"
></i>
</td>
<td>
{#if check.present && check.valid !== undefined}
<i class="bi {check.valid ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'}"></i>
<i
class="bi {check.valid
? 'bi-check-circle text-success'
: 'bi-x-circle text-warning'}"
></i>
{:else}
-
{/if}
</td>
<td>
<small class="text-muted text-truncate" title={check.value}>{check.value || '-'}</small>
<small class="text-muted text-truncate" title={check.value}
>{check.value || "-"}</small
>
{#if check.issues && check.issues.length > 0}
{#each check.issues as issue}
<div class="text-warning small">

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { ClassValue } from "svelte/elements";
import type { MxRecord } from "$lib/api/types.gen";
interface Props {

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { ScoreSummary } from "$lib/api/types.gen";
import GradeDisplay from "./GradeDisplay.svelte";
import { theme } from "$lib/stores/theme";
import GradeDisplay from "./GradeDisplay.svelte";
interface Props {
grade: string;
@ -58,13 +58,10 @@
<a href="#dns-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.dns_grade}
score={summary.dns_score}
/>
<GradeDisplay grade={summary.dns_grade} score={summary.dns_score} />
<small class="text-muted d-block">DNS</small>
</div>
</a>
@ -73,8 +70,8 @@
<a href="#authentication-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.authentication_grade}
@ -88,8 +85,8 @@
<a href="#rbl-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.blacklist_grade}
@ -103,8 +100,8 @@
<a href="#header-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.header_grade}
@ -118,13 +115,10 @@
<a href="#spam-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.spam_grade}
score={summary.spam_score}
/>
<GradeDisplay grade={summary.spam_grade} score={summary.spam_score} />
<small class="text-muted d-block">Spam Score</small>
</div>
</a>
@ -133,8 +127,8 @@
<a href="#content-details" class="text-decoration-none">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
class:bg-light={$theme === "light"}
class:bg-secondary={$theme !== "light"}
>
<GradeDisplay
grade={summary.content_grade}

View file

@ -61,14 +61,26 @@
</thead>
<tbody>
{#each Object.entries(spamassassin.test_details) as [testName, detail]}
<tr class={detail.score > 0 ? 'table-warning' : detail.score < 0 ? 'table-success' : ''}>
<tr
class={detail.score > 0
? "table-warning"
: detail.score < 0
? "table-success"
: ""}
>
<td class="font-monospace">{testName}</td>
<td class="text-end">
<span class={detail.score > 0 ? 'text-danger fw-bold' : detail.score < 0 ? 'text-success fw-bold' : 'text-muted'}>
{detail.score > 0 ? '+' : ''}{detail.score.toFixed(1)}
<span
class={detail.score > 0
? "text-danger fw-bold"
: detail.score < 0
? "text-success fw-bold"
: "text-muted"}
>
{detail.score > 0 ? "+" : ""}{detail.score.toFixed(1)}
</span>
</td>
<td class="small">{detail.description || ''}</td>
<td class="small">{detail.description || ""}</td>
</tr>
{/each}
</tbody>
@ -80,7 +92,11 @@
<strong>Tests Triggered:</strong>
<div class="mt-2">
{#each spamassassin.tests as test}
<span class="badge {$theme === 'light' ? 'bg-light text-dark' : 'bg-secondary'} me-1 mb-1">{test}</span>
<span
class="badge {$theme === 'light'
? 'bg-light text-dark'
: 'bg-secondary'} me-1 mb-1">{test}</span
>
{/each}
</div>
</div>
@ -89,7 +105,10 @@
{#if spamassassin.report}
<details class="mt-3">
<summary class="cursor-pointer fw-bold">Raw Report</summary>
<pre class="mt-2 small {$theme === 'light' ? 'bg-light' : 'bg-secondary'} p-3 rounded">{spamassassin.report}</pre>
<pre
class="mt-2 small {$theme === 'light'
? 'bg-light'
: 'bg-secondary'} p-3 rounded">{spamassassin.report}</pre>
</details>
{/if}
</div>

View file

@ -11,8 +11,8 @@
// Check if DMARC has strict policy (quarantine or reject)
const dmarcStrict = $derived(
dmarcRecord?.valid &&
dmarcRecord?.policy &&
(dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject")
dmarcRecord?.policy &&
(dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject"),
);
// Compute overall validity
@ -43,7 +43,11 @@
<span class="badge bg-secondary">SPF</span>
</div>
<div class="card-body">
<p class="card-text small text-muted mb-0">SPF specifies which mail servers are authorized to send emails on behalf of your domain. Receiving servers check the sender's IP address against your SPF record to prevent email spoofing.</p>
<p class="card-text small text-muted mb-0">
SPF specifies which mail servers are authorized to send emails on behalf of your
domain. Receiving servers check the sender's IP address against your SPF record to
prevent email spoofing.
</p>
</div>
<div class="list-group list-group-flush">
{#each spfRecords as spf, index}
@ -76,18 +80,31 @@
{:else if spf.all_qualifier === "?"}
<span class="badge bg-warning">Neutral (?all)</span>
{/if}
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))}
<div class="alert small mt-2" class:alert-warning={spf.all_qualifier !== '-'} class:alert-success={spf.all_qualifier === '-'}>
{#if spf.all_qualifier === '-'}
All unauthorized servers will be rejected. This is the recommended strict policy.
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes("redirect="))}
<div
class="alert small mt-2"
class:alert-warning={spf.all_qualifier !== "-"}
class:alert-success={spf.all_qualifier === "-"}
>
{#if spf.all_qualifier === "-"}
All unauthorized servers will be rejected. This is the
recommended strict policy.
{:else if dmarcStrict}
While your DMARC {dmarcRecord?.policy} policy provides some protection, consider using <code>-all</code> for better security with some old mailbox providers.
{:else if spf.all_qualifier === '~'}
Unauthorized servers will softfail. Consider using <code>-all</code> for stricter policy, though this rarely affects legitimate email deliverability.
{:else if spf.all_qualifier === '+'}
All servers are allowed to send email. This severely weakens email authentication. Use <code>-all</code> for strict policy.
{:else if spf.all_qualifier === '?'}
No statement about unauthorized servers. Use <code>-all</code> for strict policy to prevent spoofing.
While your DMARC {dmarcRecord?.policy} policy provides some protection,
consider using <code>-all</code> for better security with some
old mailbox providers.
{:else if spf.all_qualifier === "~"}
Unauthorized servers will softfail. Consider using <code
>-all</code
> for stricter policy, though this rarely affects legitimate
email deliverability.
{:else if spf.all_qualifier === "+"}
All servers are allowed to send email. This severely weakens
email authentication. Use <code>-all</code> for strict policy.
{:else if spf.all_qualifier === "?"}
No statement about unauthorized servers. Use <code
>-all</code
> for strict policy to prevent spoofing.
{/if}
</div>
{/if}
@ -95,14 +112,16 @@
{/if}
{#if spf.record}
<div class="mb-2">
<strong>Record:</strong><br>
<strong>Record:</strong><br />
<code class="d-block mt-1 text-break">{spf.record}</code>
</div>
{/if}
{#if spf.error}
<div class="alert alert-{spf.valid ? 'warning' : 'danger'} mb-0 mt-2">
<i class="bi bi-{spf.valid ? 'exclamation-triangle' : 'x-circle'} me-1"></i>
<strong>{spf.valid ? 'Warning:' : 'Error:'}</strong> {spf.error}
<i class="bi bi-{spf.valid ? 'exclamation-triangle' : 'x-circle'} me-1"
></i>
<strong>{spf.valid ? "Warning:" : "Error:"}</strong>
{spf.error}
</div>
{/if}
</div>

View file

@ -318,7 +318,9 @@
// BIMI
const bimiResult = report.authentication?.bimi;
if (
(dmarcRecord && dmarcRecord.valid && dmarcRecord.policy != "none") &&
dmarcRecord &&
dmarcRecord.valid &&
dmarcRecord.policy != "none" &&
(!bimiResult || bimiResult.result !== "skipped")
) {
const bimiRecord = report.dns_results?.bimi_record;
@ -523,19 +525,39 @@
{#if segment.link}
<a
href={segment.link}
class="summary-link {segment.highlight ? getColorClass(segment.highlight.color) : ''} {segment.highlight?.bold ? 'highlighted' : ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment.highlight?.monospace ? 'font-monospace' : ''}"
class="summary-link {segment.highlight
? getColorClass(segment.highlight.color)
: ''} {segment.highlight?.bold ? 'highlighted' : ''} {segment.highlight
?.emphasis
? 'fst-italic'
: ''} {segment.highlight?.monospace ? 'font-monospace' : ''}"
>
{segment.text}
</a>
{:else if segment.highlight}
<span class="{getColorClass(segment.highlight.color)} {segment.highlight.bold ? 'highlighted' : ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment.highlight?.monospace ? 'font-monospace' : ''}">
<span
class="{getColorClass(segment.highlight.color)} {segment.highlight.bold
? 'highlighted'
: ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment
.highlight?.monospace
? 'font-monospace'
: ''}"
>
{segment.text}
</span>
{:else}
{segment.text}
{/if}
{/each}
Overall, your email received a grade <GradeDisplay grade={report.grade} score={report.score} size="inline" />{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: you could have delivery issues with common providers.{:else if report.grade == "F"}: it will most likely be rejected by most providers.{:else}!{/if} Check the details below 🔽
Overall, your email received a grade <GradeDisplay
grade={report.grade}
score={report.score}
size="inline"
/>{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}:
you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}:
you could have delivery issues with common providers.{:else if report.grade == "F"}:
it will most likely be rejected by most providers.{:else}!{/if} Check the details below
🔽
</p>
{@render children?.()}
</div>

View file

@ -1,25 +1,25 @@
// Component exports
export { default as FeatureCard } from "./FeatureCard.svelte";
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
export { default as ScoreCard } from "./ScoreCard.svelte";
export { default as SummaryCard } from "./SummaryCard.svelte";
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
export { default as PendingState } from "./PendingState.svelte";
export { default as AuthenticationCard } from "./AuthenticationCard.svelte";
export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte";
export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte";
export { default as BlacklistCard } from "./BlacklistCard.svelte";
export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte";
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
export { default as TinySurvey } from "./TinySurvey.svelte";
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
export { default as GradeDisplay } from "./GradeDisplay.svelte";
export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte";
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte";
export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte";
export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte";
export { default as Logo } from "./Logo.svelte";
export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte";
export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte";
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
export { default as EmailPathCard } from "./EmailPathCard.svelte";
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
export { default as FeatureCard } from "./FeatureCard.svelte";
export { default as GradeDisplay } from "./GradeDisplay.svelte";
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
export { default as Logo } from "./Logo.svelte";
export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte";
export { default as PendingState } from "./PendingState.svelte";
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
export { default as ScoreCard } from "./ScoreCard.svelte";
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
export { default as SummaryCard } from "./SummaryCard.svelte";
export { default as TinySurvey } from "./TinySurvey.svelte";

View file

@ -1,5 +1,26 @@
import { writable } from "svelte/store";
// This file is part of the happyDeliver (R) project.
// Copyright (c) 2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { browser } from "$app/environment";
import { writable } from "svelte/store";
const getInitialTheme = () => {
if (!browser) return "light";