Add a summary after score

This commit is contained in:
nemunaire 2025-10-23 19:31:55 +07:00
commit 8b3ab541ba
11 changed files with 425 additions and 14 deletions

View file

@ -557,7 +557,7 @@ components:
example: true
importance:
type: string
enum: [required, recommended, optional]
enum: [required, recommended, optional, newsletter]
description: How important this header is for deliverability
example: "required"
issues:

View file

@ -75,7 +75,7 @@
<div class="list-group list-group-flush">
<!-- IPREV -->
{#if authentication.iprev}
<div class="list-group-item">
<div class="list-group-item" id="authentication-iprev">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.iprev.result)} {getAuthResultClass(authentication.iprev.result)} me-2 fs-5"></i>
<div>
@ -105,7 +105,7 @@
<!-- SPF (Required) -->
<div class="list-group-item">
<div class="d-flex align-items-start">
<div class="d-flex align-items-start" id="authentication-spf">
{#if authentication.spf}
<i class="bi {getAuthResultIcon(authentication.spf.result)} {getAuthResultClass(authentication.spf.result)} me-2 fs-5"></i>
<div>
@ -137,7 +137,7 @@
</div>
<!-- DKIM (Required) -->
<div class="list-group-item">
<div class="list-group-item" id="authentication-dkim">
<div class="d-flex align-items-start">
{#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>
@ -176,7 +176,7 @@
</div>
<!-- DMARC (Required) -->
<div class="list-group-item">
<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)} {getAuthResultClass(authentication.dmarc.result)} me-2 fs-5"></i>
@ -229,7 +229,7 @@
</div>
<!-- BIMI (Optional) -->
<div class="list-group-item">
<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)} {getAuthResultClass(authentication.bimi.result)} me-2 fs-5"></i>
@ -269,7 +269,7 @@
<!-- ARC (Optional) -->
{#if authentication.arc}
<div class="list-group-item">
<div class="list-group-item" id="authentication-arc">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.arc.result)} {getAuthResultClass(authentication.arc.result)} me-2 fs-5"></i>
<div>

View file

@ -9,7 +9,7 @@
</script>
{#if bimiRecord}
<div class="card mb-4">
<div class="card mb-4" id="dns-bimi">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i

View file

@ -15,7 +15,7 @@
</script>
{#if dmarcRecord}
<div class="card mb-4">
<div class="card mb-4" id="dns-dmarc">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i

View file

@ -9,7 +9,7 @@
</script>
{#if receivedChain && receivedChain.length > 0}
<div class="mb-3">
<div class="mb-3" id="email-path">
<h5>Email Path (Received Chain)</h5>
<div class="list-group">
{#each receivedChain as hop, i}

View file

@ -2,7 +2,7 @@
interface Props {
grade?: string;
score: number;
size?: "small" | "medium" | "large";
size?: "inline" | "small" | "medium" | "large";
}
let { grade, score, size = "medium" }: Props = $props();
@ -36,7 +36,8 @@
}
}
function getSizeClass(size: "small" | "medium" | "large"): string {
function getSizeClass(size: "inline" | "small" | "medium" | "large"): string {
if (size === "inline") return "fw-bold";
if (size === "small") return "fs-4";
if (size === "large") return "display-1";
return "fs-2";

View file

@ -11,7 +11,7 @@
</script>
{#if ptrRecords && ptrRecords.length > 0}
<div class="card mb-4">
<div class="card mb-4" id="dns-ptr">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i

View file

@ -14,7 +14,7 @@
</script>
{#if spfRecords && spfRecords.length > 0}
<div class="card mb-4">
<div class="card mb-4" id="dns-spf">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-2">
<i

View file

@ -0,0 +1,401 @@
<script lang="ts">
import type { Report } from "$lib/api/types.gen";
import GradeDisplay from "./GradeDisplay.svelte";
interface TextSegment {
text: string;
highlight?: {
color: "good" | "warning" | "danger";
bold?: boolean;
};
link?: string;
}
interface Props {
report: Report;
}
let { report }: Props = $props();
function buildSummary(): TextSegment[] {
const segments: TextSegment[] = [];
// Email sender information
const mailFrom = report.header_analysis?.headers?.from?.value || "an unknown sender";
const hasDkim = report.authentication?.dkim && report.authentication.dkim.length > 0;
const dkimPassed = hasDkim && report.authentication.dkim.some(d => d.result === "pass");
segments.push({ text: "Received a " });
segments.push({
text: dkimPassed ? "DKIM-signed" : "non-DKIM-signed",
highlight: { color: dkimPassed ? "good" : "danger", bold: true },
link: "#authentication-dkim"
});
segments.push({ text: " email from " });
segments.push({
text: mailFrom,
highlight: { emphasis: true }
});
// Server information and hops
const receivedChain = report.header_analysis?.received_chain;
if (receivedChain && receivedChain.length > 0) {
const firstHop = receivedChain[0];
const serverName = firstHop.from || firstHop.ip || "an unknown server";
const hopCount = receivedChain.length;
segments.push({ text: ", sent by " });
segments.push({
text: serverName,
highlight: { monospace: true },
link: "#header-details"
});
segments.push({ text: " after " });
segments.push({
text: `${hopCount-1} hop${hopCount-1 !== 1 ? "s" : ""}`,
link: "#email-path"
});
}
// Authentication status
const spfResult = report.authentication?.spf?.result;
const dmarcResult = report.authentication?.dmarc?.result;
segments.push({ text: " which is " });
if (spfResult === "pass" || dmarcResult === "pass") {
segments.push({
text: "authenticated",
highlight: { color: "good", bold: true },
link: "#authentication-details"
});
segments.push({ text: " to send email on behalf of " });
segments.push({ text: report.header_analysis?.domain_alignment?.from_domain, highlight: {monospace: true} });
} else if (spfResult && spfResult !== "none") {
segments.push({
text: "not authenticated",
highlight: { color: "danger", bold: true },
link: "#authentication-spf"
});
segments.push({ text: " (failed authentication checks)" });
} else {
segments.push({
text: "not authenticated",
highlight: { color: "warning", bold: true },
link: "#authentication-details"
});
segments.push({ text: " (lacks proper authentication)" });
}
// IP Reverse DNS (iprev) check
const iprevResult = report.authentication?.iprev;
if (iprevResult) {
segments.push({ text: ". Its reverse IP " });
if (iprevResult.result === "pass") {
segments.push({ text: "looks " });
segments.push({
text: "good",
highlight: { color: "good", bold: true },
link: "#dns-ptr"
});
} else if (iprevResult.result === "fail") {
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#dns-ptr"
});
segments.push({ text: " to pass the test" });
} else {
segments.push({ text: "returned " });
segments.push({
text: iprevResult.result,
highlight: { color: "warning", bold: true },
link: "#dns-ptr"
});
}
}
// Blacklist status
const blacklists = report.blacklists;
if (blacklists && Object.keys(blacklists).length > 0) {
const allChecks = Object.values(blacklists).flat();
const listedCount = allChecks.filter(check => check.listed).length;
segments.push({ text: ". Your server is " });
if (listedCount > 0) {
segments.push({
text: `blacklisted on ${listedCount} list${listedCount !== 1 ? "s" : ""}`,
highlight: { color: "danger", bold: true },
link: "#rbl-details"
});
} else {
segments.push({
text: "not blacklisted",
highlight: { color: "good", bold: true },
link: "#rbl-details"
});
}
}
// Domain alignment
const domainAlignment = report.header_analysis?.domain_alignment;
if (domainAlignment) {
segments.push({ text: ". Domain alignment is " });
if (domainAlignment.aligned || domainAlignment.relaxed_aligned) {
segments.push({
text: "good",
highlight: { color: "good", bold: true },
link: "#domain-alignment"
});
if (!domainAlignment.aligned) {
segments.push({ text: " using organizational domain" });
}
} else {
segments.push({
text: "misaligned",
highlight: { color: "danger", bold: true },
link: "#domain-alignment"
});
segments.push({ text: ": " });
segments.push({ text: "Return-Path", highlight: { monospace: true } });
segments.push({ text: " is set to an address of " });
segments.push({ text: report.header_analysis?.domain_alignment?.return_path_domain, highlight: { monospace: true } });
segments.push({ text: ", you should " });
segments.push({
text: "update it",
highlight: { bold: true },
link: "#domain-alignment"
});
}
}
// DMARC policy check
const dmarcRecord = report.dns_results?.dmarc_record;
if (dmarcRecord) {
if (!dmarcRecord.record) {
segments.push({ text: ". You " });
segments.push({
text: "don't have",
highlight: { color: "danger", bold: true },
link: "#dns-dmarc"
});
segments.push({ text: " a DMARC record, " });
segments.push({ text: "consider adding at least a record with the '", highlight: { bold : true } });
segments.push({ text: "none", highlight: { monospace: true, bold: true } });
segments.push({ text: "' policy", highlight: { bold : true } });
} else if (!dmarcRecord.valid) {
segments.push({ text: ". Your DMARC record has " });
segments.push({
text: "issues",
highlight: { color: "danger", bold: true },
link: "#dns-dmarc"
});
} else if (dmarcRecord.policy === "none") {
segments.push({ text: ". Your DMARC policy is " });
segments.push({
text: "set to 'none'",
highlight: { color: "warning", bold: true },
link: "#dns-dmarc"
});
segments.push({ text: ", which provides monitoring but no protection" });
} else if (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject") {
segments.push({ text: ". Your DMARC policy is '" });
segments.push({
text: dmarcRecord.policy,
highlight: { color: "good", bold: true, monospace: true },
link: "#dns-dmarc"
});
segments.push({ text: "'" });
if (dmarcRecord.policy === "reject") {
segments.push({ text: ", which is great" });
} else {
segments.push({ text: ", consider switching to reject" });
}
}
} else if (dmarcResult && dmarcResult.result === "fail") {
segments.push({ text: ". DMARC check " });
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#authentication-dmarc"
});
}
// BIMI
if (dmarcRecord.valid && dmarcRecord.policy != "none") {
const bimiResult = report.authentication?.bimi;
const bimiRecord = report.dns_results?.bimi_record;
if (bimiRecord?.valid) {
segments.push({ text: ". Your domain includes " });
segments.push({
text: "BIMI",
highlight: { color: "good", bold: true },
link: "#dns-bimi"
});
segments.push({ text: " for brand indicator display" });
} else if (bimiResult && bimiResult.details.indexOf("(No BIMI records found)") >= 0) {
segments.push({ text: ". Your domain has no " });
segments.push({
text: "BIMI record",
highlight: { color: "warning", bold: true },
link: "#dns-bimi"
});
} else if (bimiResult || bimiRecord) {
segments.push({ text: ". Your domain has " });
segments.push({
text: "BIMI configured with issues",
highlight: { color: "warning", bold: true },
link: "#dns-bimi"
});
}
}
// ARC
const arcResult = report.authentication?.arc;
if (arcResult && arcResult.result !== "none") {
segments.push({ text: ". " });
segments.push({
text: "ARC chain validation",
link: "#authentication-arc"
});
segments.push({ text: " " });
if (arcResult.chain_valid) {
segments.push({
text: "passed",
highlight: { color: "good", bold: true }
});
segments.push({ text: ` with ${arcResult.chain_length} set${arcResult.chain_length !== 1 ? "s" : ""}, indicating proper email forwarding` });
} else {
segments.push({
text: "failed",
highlight: { color: "danger", bold: true }
});
segments.push({ text: ", which may indicate issues with email forwarding" });
}
}
// Newsletter/marketing headers check
const headers = report.header_analysis?.headers;
const listUnsubscribe = headers?.["list-unsubscribe"];
const listUnsubscribePost = headers?.["list-unsubscribe-post"];
const hasNewsletterHeaders = (listUnsubscribe?.importance === "newsletter" && listUnsubscribe?.present) ||
(listUnsubscribePost?.importance === "newsletter" && listUnsubscribePost?.present);
if (!hasNewsletterHeaders && (listUnsubscribe?.importance === "newsletter" || listUnsubscribePost?.importance === "newsletter")) {
segments.push({ text: ". This email is " });
segments.push({
text: "missing unsubscribe headers",
highlight: { color: "warning", bold: true },
link: "#header-details"
});
segments.push({ text: " and is " });
segments.push({
text: "not suitable for marketing campaigns",
highlight: { bold: true }
});
}
// Content/spam assessment
const spamAssassin = report.spamassassin;
const contentScore = report.summary?.content_score || 0;
segments.push({ text: ". " });
if (spamAssassin?.is_spam) {
segments.push({ text: "Content is " });
segments.push({
text: "flagged as spam",
highlight: { color: "danger", bold: true },
link: "#spam-details"
});
segments.push({ text: " and needs review" });
} else if (contentScore < 50) {
segments.push({ text: "Content quality " });
segments.push({
text: "needs improvement",
highlight: { color: "warning", bold: true },
link: "#content-details"
});
} else if (contentScore >= 100) {
segments.push({ text: "Content " });
segments.push({
text: "looks great",
highlight: { color: "good", bold: true },
link: "#content-details"
});
} else if (contentScore >= 80) {
segments.push({ text: "Content " });
segments.push({
text: "looks good",
highlight: { color: "good", bold: true },
link: "#content-details"
});
} else {
segments.push({ text: "Content " });
segments.push({
text: "should be reviewed",
highlight: { color: "warning", bold: true },
link: "#content-details"
});
}
segments.push({ text: "." });
return segments;
}
function getColorClass(color: "good" | "warning" | "danger"): string {
switch (color) {
case "good":
return "text-success";
case "warning":
return "text-warning";
case "danger":
return "text-danger";
}
}
const summarySegments = $derived(buildSummary());
</script>
<style>
.summary-link {
text-decoration: none;
transition: opacity 0.2s ease;
}
.summary-link:hover {
opacity: 0.8;
text-decoration: underline;
}
.highlighted {
font-weight: 600;
}
</style>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body p-4">
<h5 class="card-title mb-3">
<i class="bi bi-card-text me-2"></i>
Summary
</h5>
<p class="card-text text-muted mb-0" style="line-height: 1.8;">
{#each summarySegments as segment}
{#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' : ''}"
>
{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' : ''}">
{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 🎉{/if}! Check the details below 🔽
</p>
</div>
</div>

View file

@ -2,6 +2,7 @@
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";

View file

@ -5,6 +5,7 @@
import type { Test, Report } from "$lib/api/types.gen";
import {
ScoreCard,
SummaryCard,
SpamAssassinCard,
PendingState,
AuthenticationCard,
@ -138,6 +139,13 @@
</div>
</div>
<!-- Summary -->
<div class="row mb-4">
<div class="col-12">
<SummaryCard {report} />
</div>
</div>
<!-- DNS Records -->
{#if report.dns_results}
<div class="row mb-4" id="dns">