From 8b3ab541ba15b779a2834e55b1d4b97acf0c7bb1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Oct 2025 19:31:55 +0700 Subject: [PATCH] Add a summary after score --- api/openapi.yaml | 2 +- .../lib/components/AuthenticationCard.svelte | 12 +- .../lib/components/BimiRecordDisplay.svelte | 2 +- .../lib/components/DmarcRecordDisplay.svelte | 2 +- web/src/lib/components/EmailPathCard.svelte | 2 +- web/src/lib/components/GradeDisplay.svelte | 5 +- .../lib/components/PtrRecordsDisplay.svelte | 2 +- .../lib/components/SpfRecordsDisplay.svelte | 2 +- web/src/lib/components/SummaryCard.svelte | 401 ++++++++++++++++++ web/src/lib/components/index.ts | 1 + web/src/routes/test/[test]/+page.svelte | 8 + 11 files changed, 425 insertions(+), 14 deletions(-) create mode 100644 web/src/lib/components/SummaryCard.svelte diff --git a/api/openapi.yaml b/api/openapi.yaml index 5484f9e..6be919d 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -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: diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index e46fe9e..c43be7d 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -75,7 +75,7 @@
{#if authentication.iprev} -
+
@@ -105,7 +105,7 @@
-
+
{#if authentication.spf}
@@ -137,7 +137,7 @@
-
+
{#if authentication.dkim && authentication.dkim.length > 0} @@ -176,7 +176,7 @@
-
+
{#if authentication.dmarc} @@ -229,7 +229,7 @@
-
+
{#if authentication.bimi && authentication.bimi.result != "none"} @@ -269,7 +269,7 @@ {#if authentication.arc} -
+
diff --git a/web/src/lib/components/BimiRecordDisplay.svelte b/web/src/lib/components/BimiRecordDisplay.svelte index 0d7a1b9..f9aee88 100644 --- a/web/src/lib/components/BimiRecordDisplay.svelte +++ b/web/src/lib/components/BimiRecordDisplay.svelte @@ -9,7 +9,7 @@ {#if bimiRecord} -
+
{#if dmarcRecord} -
+
{#if receivedChain && receivedChain.length > 0} -
+
Email Path (Received Chain)
{#each receivedChain as hop, i} diff --git a/web/src/lib/components/GradeDisplay.svelte b/web/src/lib/components/GradeDisplay.svelte index 322259b..b503fec 100644 --- a/web/src/lib/components/GradeDisplay.svelte +++ b/web/src/lib/components/GradeDisplay.svelte @@ -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"; diff --git a/web/src/lib/components/PtrRecordsDisplay.svelte b/web/src/lib/components/PtrRecordsDisplay.svelte index 4ba7a81..66b4940 100644 --- a/web/src/lib/components/PtrRecordsDisplay.svelte +++ b/web/src/lib/components/PtrRecordsDisplay.svelte @@ -11,7 +11,7 @@ {#if ptrRecords && ptrRecords.length > 0} -
+
{#if spfRecords && spfRecords.length > 0} -
+
+ 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()); + + + + +
+
+
+ + Summary +
+

+ {#each summarySegments as segment} + {#if segment.link} + + {segment.text} + + {:else if segment.highlight} + + {segment.text} + + {:else} + {segment.text} + {/if} + {/each} + Overall, your email received a grade {#if report.grade == "A" || report.grade == "A+"}, well done 🎉{/if}! Check the details below 🔽 +

+
+
diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index d3b7909..8b83ae5 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -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"; diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 28a140a..c79b9f4 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -5,6 +5,7 @@ import type { Test, Report } from "$lib/api/types.gen"; import { ScoreCard, + SummaryCard, SpamAssassinCard, PendingState, AuthenticationCard, @@ -138,6 +139,13 @@
+ +
+
+ +
+
+ {#if report.dns_results}