Tests design and descriptions

This commit is contained in:
nemunaire 2025-10-22 22:41:11 +07:00
commit a97729fea6
7 changed files with 230 additions and 164 deletions

View file

@ -74,14 +74,21 @@ body {
/* Custom card styling */ /* Custom card styling */
.card { .card {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transition:
transform 0.2s ease, transform 0.2s ease,
box-shadow 0.2s ease; box-shadow 0.2s ease;
} }
.card:hover { .card:not(.fade-in .card) {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.fade-in .card:not(.card .card) {
border: none;
}
.card:hover:not(.fade-in .card) {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
} }

View file

@ -72,10 +72,9 @@
</span> </span>
</h4> </h4>
</div> </div>
<div class="card-body"> <div class="list-group list-group-flush">
<div class="row row-cols-1">
<!-- SPF (Required) --> <!-- SPF (Required) -->
<div class="col mb-3"> <div class="list-group-item">
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
{#if authentication.spf} {#if authentication.spf}
<i class="bi {getAuthResultIcon(authentication.spf.result)} {getAuthResultClass(authentication.spf.result)} me-2 fs-5"></i> <i class="bi {getAuthResultIcon(authentication.spf.result)} {getAuthResultClass(authentication.spf.result)} me-2 fs-5"></i>
@ -108,7 +107,7 @@
</div> </div>
<!-- DKIM (Required) --> <!-- DKIM (Required) -->
<div class="col mb-3"> <div class="list-group-item">
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
{#if authentication.dkim && authentication.dkim.length > 0} {#if authentication.dkim && authentication.dkim.length > 0}
<i class="bi {getAuthResultIcon(authentication.dkim[0].result)} {getAuthResultClass(authentication.dkim[0].result)} me-2 fs-5"></i> <i class="bi {getAuthResultIcon(authentication.dkim[0].result)} {getAuthResultClass(authentication.dkim[0].result)} me-2 fs-5"></i>
@ -147,7 +146,7 @@
</div> </div>
<!-- DMARC (Required) --> <!-- DMARC (Required) -->
<div class="col mb-3"> <div class="list-group-item">
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
{#if authentication.dmarc} {#if authentication.dmarc}
<i class="bi {getAuthResultIcon(authentication.dmarc.result)} {getAuthResultClass(authentication.dmarc.result)} me-2 fs-5"></i> <i class="bi {getAuthResultIcon(authentication.dmarc.result)} {getAuthResultClass(authentication.dmarc.result)} me-2 fs-5"></i>
@ -200,7 +199,7 @@
</div> </div>
<!-- BIMI (Optional) --> <!-- BIMI (Optional) -->
<div class="col mb-3"> <div class="list-group-item">
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
{#if authentication.bimi && authentication.bimi.result != "none"} {#if authentication.bimi && authentication.bimi.result != "none"}
<i class="bi {getAuthResultIcon(authentication.bimi.result)} {getAuthResultClass(authentication.bimi.result)} me-2 fs-5"></i> <i class="bi {getAuthResultIcon(authentication.bimi.result)} {getAuthResultClass(authentication.bimi.result)} me-2 fs-5"></i>
@ -240,7 +239,7 @@
<!-- ARC (Optional) --> <!-- ARC (Optional) -->
{#if authentication.arc} {#if authentication.arc}
<div class="col mb-3"> <div class="list-group-item">
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.arc.result)} {getAuthResultClass(authentication.arc.result)} me-2 fs-5"></i> <i class="bi {getAuthResultIcon(authentication.arc.result)} {getAuthResultClass(authentication.arc.result)} me-2 fs-5"></i>
<div> <div>
@ -258,6 +257,5 @@
</div> </div>
</div> </div>
{/if} {/if}
</div>
</div> </div>
</div> </div>

View file

@ -35,32 +35,24 @@
<div class="row row-cols-1 row-cols-lg-2"> <div class="row row-cols-1 row-cols-lg-2">
{#each Object.entries(blacklists) as [ip, checks]} {#each Object.entries(blacklists) as [ip, checks]}
<div class="col mb-3"> <div class="col mb-3">
<h6 class="text-muted"> <h5 class="text-muted">
<i class="bi bi-hdd-network me-1"></i> <i class="bi bi-hdd-network me-1"></i>
{ip} {ip}
</h6> </h5>
<div class="table-responsive"> <table class="table table-sm table-striped table-hover mb-0">
<table class="table table-sm"> <tbody>
<thead> {#each checks as check}
<tr> <tr>
<th>RBL</th> <td title={check.response || '-'}>
<th>Status</th> <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>
</tr> </tr>
</thead> {/each}
<tbody> </tbody>
{#each checks as check} </table>
<tr>
<td><code>{check.rbl}</code></td>
<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>
</tr>
{/each}
</tbody>
</table>
</div>
</div> </div>
{/each} {/each}
</div> </div>

View file

@ -48,8 +48,10 @@
{/if} {/if}
<!-- Return-Path Domain Section --> <!-- Return-Path Domain Section -->
<div class="mb-3"> <div class="mb-3 d-flex align-items-center gap-2">
<strong>Return-Path Domain:</strong> <code>{dnsResults.rp_domain || dnsResults.from_domain}</code> <h4 class="mb-0">
Return-Path Domain: <code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain} {#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> Different from From domain</span> <span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Different from From domain</span>
<small> <small>
@ -64,6 +66,7 @@
<!-- MX Records for Return-Path Domain --> <!-- MX Records for Return-Path Domain -->
{#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0} {#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0}
<MxRecordsDisplay <MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.rp_mx_records} mxRecords={dnsResults.rp_mx_records}
title="Mail Exchange Records for Return-Path Domain" title="Mail Exchange Records for Return-Path Domain"
description="These MX records handle bounce messages and non-delivery reports." description="These MX records handle bounce messages and non-delivery reports."
@ -72,14 +75,27 @@
<!-- SPF Records (for Return-Path Domain) --> <!-- SPF Records (for Return-Path Domain) -->
{#if dnsResults.spf_records && dnsResults.spf_records.length > 0} {#if dnsResults.spf_records && dnsResults.spf_records.length > 0}
<div class="mb-4"> {@const spfIsValid = dnsResults.spf_records.reduce((acc, r) => acc && r.valid, true)}
<h5 class="text-muted mb-2"> <div class="card mb-4">
<span class="badge bg-secondary">SPF</span> Sender Policy Framework <div class="card-header d-flex justify-content-between align-items-center">
</h5> <h5 class="text-muted mb-2">
<p class="small text-muted mb-2">SPF validates the Return-Path (envelope sender) domain.</p> <i
{#each dnsResults.spf_records as spf, index} class="bi"
<div class="card mb-2"> class:bi-check-circle-fill={spfIsValid}
<div class="card-body"> class:text-success={spfIsValid}
class:bi-x-circle-fill={!spfIsValid}
class:text-danger={!spfIsValid}
></i>
Sender Policy Framework
</h5>
<span class="badge bg-secondary">SPF</span>
</div>
<div class="card-body pb-0">
<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 dnsResults.spf_records as spf, index}
<div class="list-group-item">
{#if spf.domain} {#if spf.domain}
<div class="mb-2"> <div class="mb-2">
<strong>Domain:</strong> <code>{spf.domain}</code> <strong>Domain:</strong> <code>{spf.domain}</code>
@ -109,16 +125,18 @@
</div> </div>
{/if} {/if}
</div> </div>
</div> {/each}
{/each} </div>
</div> </div>
{/if} {/if}
<hr> <hr class="my-4">
<!-- From Domain Section --> <!-- From Domain Section -->
<div class="mb-3"> <div class="mb-3 d-flex align-items-center gap-2">
<strong>From Domain:</strong> <code>{dnsResults.from_domain}</code> <h4 class="mb-0">
From Domain: <code>{dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain} {#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> Different from Return-Path domain</span> <span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Different from Return-Path domain</span>
{/if} {/if}
@ -127,6 +145,7 @@
<!-- MX Records for From Domain --> <!-- MX Records for From Domain -->
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0} {#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
<MxRecordsDisplay <MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.from_mx_records} mxRecords={dnsResults.from_mx_records}
title="Mail Exchange Records for From Domain" title="Mail Exchange Records for From Domain"
description="These MX records handle replies to emails sent from this domain." description="These MX records handle replies to emails sent from this domain."
@ -135,13 +154,27 @@
<!-- DKIM Records --> <!-- DKIM Records -->
{#if dnsResults.dkim_records && dnsResults.dkim_records.length > 0} {#if dnsResults.dkim_records && dnsResults.dkim_records.length > 0}
<div class="mb-4"> {@const dkimIsValid = dnsResults.dkim_records.reduce((acc, r) => acc && r.valid, true)}
<h5 class="text-muted mb-2"> <div class="card mb-4">
<span class="badge bg-secondary">DKIM</span> DomainKeys Identified Mail <div class="card-header d-flex justify-content-between align-items-center">
</h5> <h5 class="text-muted mb-0">
{#each dnsResults.dkim_records as dkim} <i
<div class="card mb-2"> class="bi"
<div class="card-body"> class:bi-check-circle-fill={dkimIsValid}
class:text-success={dkimIsValid}
class:bi-x-circle-fill={!dkimIsValid}
class:text-danger={!dkimIsValid}
></i>
DomainKeys Identified Mail
</h5>
<span class="badge bg-secondary">DKIM</span>
</div>
<div class="card-body pb-0">
<p class="card-text small text-muted mb-0">DKIM cryptographically signs your emails, proving they haven't been tampered with in transit. Receiving servers verify this signature against your DNS records.</p>
</div>
<div class="list-group list-group-flush">
{#each dnsResults.dkim_records as dkim}
<div class="list-group-item">
<div class="mb-2"> <div class="mb-2">
<strong>Selector:</strong> <code>{dkim.selector}</code> <strong>Selector:</strong> <code>{dkim.selector}</code>
<strong class="ms-3">Domain:</strong> <code>{dkim.domain}</code> <strong class="ms-3">Domain:</strong> <code>{dkim.domain}</code>
@ -166,59 +199,79 @@
</div> </div>
{/if} {/if}
</div> </div>
</div> {/each}
{/each} </div>
</div> </div>
{/if} {/if}
<!-- DMARC Record --> <!-- DMARC Record -->
{#if dnsResults.dmarc_record} {#if dnsResults.dmarc_record}
<div class="mb-4"> <div class="card mb-4">
<h5 class="text-muted mb-2"> <div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-secondary">DMARC</span> Domain-based Message Authentication <h5 class="text-muted mb-0">
</h5> <i
<div class="card"> class="bi"
<div class="card-body"> class:bi-check-circle-fill={dnsResults.dmarc_record.valid}
<div class="mb-2"> class:text-success={dnsResults.dmarc_record.valid}
<strong>Status:</strong> class:bi-x-circle-fill={!dnsResults.dmarc_record.valid}
{#if dnsResults.dmarc_record.valid} class:text-danger={!dnsResults.dmarc_record.valid}
<span class="badge bg-success">Valid</span> ></i>
{:else} Domain-based Message Authentication
<span class="badge bg-danger">Invalid</span> </h5>
{/if} <span class="badge bg-secondary">DMARC</span>
</div> </div>
{#if dnsResults.dmarc_record.policy} <div class="card-body">
<div class="mb-2"> <p class="card-text small text-muted mb-2">DMARC builds on SPF and DKIM by telling receiving servers what to do with emails that fail authentication checks. It also enables reporting so you can monitor your email security.</p>
<strong>Policy:</strong> <div class="mb-2">
<span class="badge {dnsResults.dmarc_record.policy === 'reject' ? 'bg-success' : dnsResults.dmarc_record.policy === 'quarantine' ? 'bg-warning' : 'bg-secondary'}"> <strong>Status:</strong>
{dnsResults.dmarc_record.policy} {#if dnsResults.dmarc_record.valid}
</span> <span class="badge bg-success">Valid</span>
</div> {:else}
{/if} <span class="badge bg-danger">Invalid</span>
{#if dnsResults.dmarc_record.record}
<div class="mb-2">
<strong>Record:</strong><br>
<code class="d-block mt-1 text-break">{dnsResults.dmarc_record.record}</code>
</div>
{/if}
{#if dnsResults.dmarc_record.error}
<div class="text-danger">
<strong>Error:</strong> {dnsResults.dmarc_record.error}
</div>
{/if} {/if}
</div> </div>
{#if dnsResults.dmarc_record.policy}
<div class="mb-2">
<strong>Policy:</strong>
<span class="badge {dnsResults.dmarc_record.policy === 'reject' ? 'bg-success' : dnsResults.dmarc_record.policy === 'quarantine' ? 'bg-warning' : 'bg-secondary'}">
{dnsResults.dmarc_record.policy}
</span>
</div>
{/if}
{#if dnsResults.dmarc_record.record}
<div class="mb-2">
<strong>Record:</strong><br>
<code class="d-block mt-1 text-break">{dnsResults.dmarc_record.record}</code>
</div>
{/if}
{#if dnsResults.dmarc_record.error}
<div class="text-danger">
<strong>Error:</strong> {dnsResults.dmarc_record.error}
</div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}
<!-- BIMI Record --> <!-- BIMI Record -->
{#if dnsResults.bimi_record} {#if dnsResults.bimi_record}
<div class="mb-4"> <div class="card mb-4">
<h5 class="text-muted mb-2"> <div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-secondary">BIMI</span> Brand Indicators for Message Identification <h5 class="text-muted mb-0">
</h5> <i
class="bi"
class:bi-check-circle-fill={dnsResults.bimi_record.valid}
class:text-success={dnsResults.bimi_record.valid}
class:bi-x-circle-fill={!dnsResults.bimi_record.valid}
class:text-danger={!dnsResults.bimi_record.valid}
></i>
Brand Indicators for Message Identification
</h5>
<span class="badge bg-secondary">BIMI</span>
</div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p class="card-text small text-muted mb-2">BIMI allows your brand logo to be displayed next to your emails in supported mail clients. Requires strong DMARC enforcement (quarantine or reject policy) and optionally a Verified Mark Certificate (VMC).</p>
<div class="mb-2"> <div class="mb-2">
<strong>Selector:</strong> <code>{dnsResults.bimi_record.selector}</code> <strong>Selector:</strong> <code>{dnsResults.bimi_record.selector}</code>
<strong class="ms-3">Domain:</strong> <code>{dnsResults.bimi_record.domain}</code> <strong class="ms-3">Domain:</strong> <code>{dnsResults.bimi_record.domain}</code>

View file

@ -34,7 +34,7 @@
<div class="card-body"> <div class="card-body">
{#if headerAnalysis.issues && headerAnalysis.issues.length > 0} {#if headerAnalysis.issues && headerAnalysis.issues.length > 0}
<div class="mb-3"> <div class="mb-3">
<h6>Issues</h6> <h5>Issues</h5>
{#each headerAnalysis.issues as issue} {#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 class="d-flex justify-content-between align-items-start">
@ -56,50 +56,59 @@
{/if} {/if}
{#if headerAnalysis.domain_alignment} {#if headerAnalysis.domain_alignment}
<div class="mb-3" id="domain-alignment"> <div class="card mb-3" id="domain-alignment">
<h6>Domain Alignment</h6> <div class="card-header">
<div class="card"> <h5 class="mb-0">
<div class="card-body"> <i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill text-success' : 'bi-x-circle-fill text-danger'}"></i>
<div class="row"> Domain Alignment
<div class="col-md-4"> </h5>
<small class="text-muted">Aligned</small> </div>
<div> <div class="card-body">
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'} me-1"></i> <p class="card-text small text-muted mb-3">
Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path). Proper alignment is crucial for DMARC compliance and helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers.
</p>
<div class="row">
<div class="col-md-4">
<small class="text-muted">Aligned</small>
<div>
<span class="badge" class:bg-success={headerAnalysis.domain_alignment.aligned} class:bg-danger={!headerAnalysis.domain_alignment.aligned}>
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
<strong>{headerAnalysis.domain_alignment.aligned ? 'Yes' : 'No'}</strong> <strong>{headerAnalysis.domain_alignment.aligned ? 'Yes' : 'No'}</strong>
</div> </span>
</div>
<div class="col-md-4">
<small class="text-muted">From Domain</small>
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
</div>
<div class="col-md-4">
<small class="text-muted">Return-Path Domain</small>
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
</div> </div>
</div> </div>
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} <div class="col-md-4">
<div class="mt-2"> <small class="text-muted">From Domain</small>
{#each headerAnalysis.domain_alignment.issues as issue} <div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
<div class="text-warning small"> </div>
<i class="bi bi-exclamation-triangle me-1"></i> <div class="col-md-4">
{issue} <small class="text-muted">Return-Path Domain</small>
</div> <div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
{/each} </div>
</div>
{/if}
</div> </div>
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
<div class="mt-2">
{#each headerAnalysis.domain_alignment.issues as issue}
<div class="text-warning small">
<i class="bi bi-exclamation-triangle me-1"></i>
{issue}
</div>
{/each}
</div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}
{#if headerAnalysis.headers && Object.keys(headerAnalysis.headers).length > 0} {#if headerAnalysis.headers && Object.keys(headerAnalysis.headers).length > 0}
<div class="mt-3"> <div class="mt-3">
<h6>Headers</h6> <h5 style="margin-bottom: -1.3em">Headers</h5>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Header</th> <th></th>
<th>When?</th>
<th>Present</th> <th>Present</th>
<th>Valid</th> <th>Valid</th>
<th>Value</th> <th>Value</th>
@ -115,10 +124,12 @@
<tr> <tr>
<td> <td>
<code>{name}</code> <code>{name}</code>
</td>
<td>
{#if check.importance} {#if check.importance}
<span class="badge bg-{check.importance === 'required' ? 'danger' : check.importance === 'recommended' ? 'warning' : 'secondary'} ms-1"> <small class="text-{check.importance === 'required' ? 'danger' : check.importance === 'recommended' ? 'warning' : 'secondary'}">
{check.importance} {check.importance}
</span> </small>
{/if} {/if}
</td> </td>
<td> <td>
@ -132,7 +143,7 @@
{/if} {/if}
</td> </td>
<td> <td>
<small class="text-muted text-break">{check.value || '-'}</small> <small class="text-muted text-truncate" title={check.value}>{check.value || '-'}</small>
{#if check.issues && check.issues.length > 0} {#if check.issues && check.issues.length > 0}
{#each check.issues as issue} {#each check.issues as issue}
<div class="text-warning small"> <div class="text-warning small">

View file

@ -1,49 +1,54 @@
<script lang="ts"> <script lang="ts">
import type { ClassValue } from 'svelte/elements';
import type { MXRecord } from "$lib/api/types.gen"; import type { MXRecord } from "$lib/api/types.gen";
interface Props { interface Props {
class: ClassValue;
mxRecords: MXRecord[]; mxRecords: MXRecord[];
title: string; title: string;
description?: string; description?: string;
} }
let { mxRecords, title, description }: Props = $props(); let { class: className, mxRecords, title, description }: Props = $props();
let mxsAreValids = $derived(mxRecords.reduce((acc, r) => acc && r.valid, true));
</script> </script>
<div class="mb-4"> <div class="card {className}">
<h5 class="text-muted mb-2"> <div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-secondary">MX</span> {title} <h5 class="text-muted mb-0">
</h5> <i
{#if description} class="bi"
<p class="small text-muted mb-2">{description}</p> class:bi-check-circle-fill={mxsAreValids}
{/if} class:text-success={mxsAreValids}
<div class="table-responsive"> class:bi-x-circle-fill={!mxsAreValids}
<table class="table table-sm table-bordered"> class:text-danger={!mxsAreValids}
<thead> ></i>
<tr> {title}
<th>Priority</th> </h5>
<th>Host</th> <span class="badge bg-secondary">MX</span>
<th>Status</th> </div>
</tr> <div class="card-body pb-0">
</thead> {#if description}
<tbody> <p class="card-text small text-muted mb-0">{description}</p>
{#each mxRecords as mx} {/if}
<tr> </div>
<td>{mx.priority}</td> <div class="list-group list-group-flush">
<td><code>{mx.host}</code></td> {#each mxRecords as mx}
<td> <div class="list-group-item">
{#if mx.valid} <div class="d-flex gap-2 align-items-center">
<span class="badge bg-success">Valid</span> {#if mx.valid}
{:else} <span class="badge bg-success">Valid</span>
<span class="badge bg-danger">Invalid</span> {:else}
{#if mx.error} <span class="badge bg-danger">Invalid</span>
<br><small class="text-danger">{mx.error}</small> {/if}
{/if} <div>Host: <code>{mx.host}</code></div>
{/if} <div>Priority: <strong>{mx.priority}</strong></div>
</td> </div>
</tr> {#if mx.error}
{/each} <small class="text-danger">{mx.error}</small>
</tbody> {/if}
</table> </div>
{/each}
</div> </div>
</div> </div>

View file

@ -12,7 +12,7 @@
let { spamassassin, spamGrade, spamScore }: Props = $props(); let { spamassassin, spamGrade, spamScore }: Props = $props();
</script> </script>
<div class="card"> <div class="card shadow-sm">
<div class="card-header bg-white"> <div class="card-header bg-white">
<h4 class="mb-0 d-flex justify-content-between align-items-center"> <h4 class="mb-0 d-flex justify-content-between align-items-center">
<span> <span>