Fix typescript/svelte checks

This commit is contained in:
nemunaire 2025-10-24 17:20:35 +07:00
commit 474f25007b
17 changed files with 199 additions and 155 deletions

View file

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { Authentication, DNSResults, ReportSummary } from "$lib/api/types.gen"; import type { AuthenticationResults, DnsResults } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score"; import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte"; import GradeDisplay from "./GradeDisplay.svelte";
interface Props { interface Props {
authentication: Authentication; authentication: AuthenticationResults;
authenticationGrade?: string; authenticationGrade?: string;
authenticationScore?: number; authenticationScore?: number;
dnsResults?: DNSResults; dnsResults?: DnsResults;
} }
let { authentication, authenticationGrade, authenticationScore, dnsResults }: Props = $props(); let { authentication, authenticationGrade, authenticationScore, dnsResults }: Props = $props();
@ -132,10 +132,10 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i> <i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div> <div>
<strong>SPF</strong> <strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}"> <span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')} {getAuthResultText('missing')}
</span> </span>
<div class="text-muted small">SPF record is required for proper email authentication</div> <div class="text-muted small">SPF record is required for proper email authentication</div>
@ -171,10 +171,10 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i> <i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div> <div>
<strong>DKIM</strong> <strong>DKIM</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}"> <span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')} {getAuthResultText('missing')}
</span> </span>
<div class="text-muted small">DKIM signature is required for proper email authentication</div> <div class="text-muted small">DKIM signature is required for proper email authentication</div>
@ -214,7 +214,7 @@
{/if} {/if}
<!-- X-Aligned-From (Disabled) --> <!-- X-Aligned-From (Disabled) -->
{#if false && authentication.x_aligned_from} {#if authentication.x_aligned_from}
<div class="list-group-item" id="authentication-x-aligned-from"> <div class="list-group-item" id="authentication-x-aligned-from">
<div class="d-flex align-items-start"> <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> <i class="bi {getAuthResultIcon(authentication.x_aligned_from.result, false)} {getAuthResultClass(authentication.x_aligned_from.result, false)} me-2 fs-5"></i>
@ -253,7 +253,7 @@
<span class="text-muted">{authentication.dmarc.domain}</span> <span class="text-muted">{authentication.dmarc.domain}</span>
</div> </div>
{/if} {/if}
{#snippet DMARCPolicy(policy)} {#snippet DMARCPolicy(policy: string)}
<div class="small"> <div class="small">
<strong>Policy:</strong> <strong>Policy:</strong>
<span <span
@ -268,10 +268,10 @@
</div> </div>
{/snippet} {/snippet}
{#if authentication.dmarc.result != "none"} {#if authentication.dmarc.result != "none"}
{#if authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0} {#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")} {@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
{@render DMARCPolicy(policy)} {@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain} {:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)} {@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{/if} {/if}
{/if} {/if}
@ -280,10 +280,10 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i> <i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div> <div>
<strong>DMARC</strong> <strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}"> <span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')} {getAuthResultText('missing')}
</span> </span>
<div class="text-muted small">DMARC policy is required for proper email authentication</div> <div class="text-muted small">DMARC policy is required for proper email authentication</div>
@ -296,10 +296,10 @@
<div class="list-group-item" id="authentication-bimi"> <div class="list-group-item" id="authentication-bimi">
<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, false)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"></i>
<div> <div>
<strong>BIMI</strong> <strong>BIMI</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result)}"> <span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result, false)}">
{authentication.bimi.result} {authentication.bimi.result}
</span> </span>
{#if authentication.bimi.details} {#if authentication.bimi.details}
@ -335,10 +335,10 @@
{#if authentication.arc} {#if authentication.arc}
<div class="list-group-item" id="authentication-arc"> <div class="list-group-item" id="authentication-arc">
<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, false)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"></i>
<div> <div>
<strong>ARC</strong> <strong>ARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result)}"> <span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result, false)}">
{authentication.arc.result} {authentication.arc.result}
</span> </span>
{#if authentication.arc.chain_length} {#if authentication.arc.chain_length}

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { BIMIRecord } from "$lib/api/types.gen"; import type { BimiRecord } from "$lib/api/types.gen";
interface Props { interface Props {
bimiRecord?: BIMIRecord; bimiRecord?: BimiRecord;
} }
let { bimiRecord }: Props = $props(); let { bimiRecord }: Props = $props();

View file

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { RBLCheck, ReceivedHop } from "$lib/api/types.gen"; import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score"; import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte"; import GradeDisplay from "./GradeDisplay.svelte";
import EmailPathCard from "./EmailPathCard.svelte"; import EmailPathCard from "./EmailPathCard.svelte";
interface Props { interface Props {
blacklists: Record<string, RBLCheck[]>; blacklists: Record<string, BlacklistCheck[]>;
blacklistGrade?: string; blacklistGrade?: string;
blacklistScore?: number; blacklistScore?: number;
receivedChain?: ReceivedHop[]; receivedChain?: ReceivedHop[];

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { DKIMRecord } from "$lib/api/types.gen"; import type { DkimRecord } from "$lib/api/types.gen";
interface Props { interface Props {
dkimRecords?: DKIMRecord[]; dkimRecords?: DkimRecord[];
} }
let { dkimRecords }: Props = $props(); let { dkimRecords }: Props = $props();

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { DMARCRecord } from "$lib/api/types.gen"; import type { DmarcRecord } from "$lib/api/types.gen";
interface Props { interface Props {
dmarcRecord?: DMARCRecord; dmarcRecord?: DmarcRecord;
} }
let { dmarcRecord }: Props = $props(); let { dmarcRecord }: Props = $props();

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { DomainAlignment, DNSResults, ReceivedHop } from "$lib/api/types.gen"; import type { DomainAlignment, DnsResults, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score"; import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte"; import GradeDisplay from "./GradeDisplay.svelte";
import MxRecordsDisplay from "./MxRecordsDisplay.svelte"; import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
@ -12,7 +12,7 @@
interface Props { interface Props {
domainAlignment?: DomainAlignment; domainAlignment?: DomainAlignment;
dnsResults?: DNSResults; dnsResults?: DnsResults;
dnsGrade?: string; dnsGrade?: string;
dnsScore?: number; dnsScore?: number;
receivedChain?: ReceivedHop[]; receivedChain?: ReceivedHop[];

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
grade?: string; grade?: string;
score: number; score?: number;
size?: "inline" | "small" | "medium" | "large"; size?: "inline" | "small" | "medium" | "large";
} }

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { AuthResult, DMARCRecord, HeaderAnalysis } from "$lib/api/types.gen"; import type { AuthResult, DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score"; import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte"; import GradeDisplay from "./GradeDisplay.svelte";
interface Props { interface Props {
dmarcRecord: DMARCRecord; dmarcRecord?: DmarcRecord;
headerAnalysis: HeaderAnalysis; headerAnalysis: HeaderAnalysis;
headerGrade?: string; headerGrade?: string;
headerScore?: number; headerScore?: number;
@ -62,7 +62,7 @@
<div class="card-header"> <div class="card-header">
<h5 class="mb-0"> <h5 class="mb-0">
{#if xAlignedFrom} {#if xAlignedFrom}
<i class="bi {xAlignedFrom == "pass" ? 'bi-check-circle-fill text-success' : 'bi-x-circle-fill text-danger'}"></i> <i class="bi {xAlignedFrom.result == "pass" ? 'bi-check-circle-fill text-success' : 'bi-x-circle-fill text-danger'}"></i>
{:else} {:else}
<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>
{/if} {/if}

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { ClassValue } from 'svelte/elements'; 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; class: ClassValue;
mxRecords: MXRecord[]; mxRecords: MxRecord[];
title: string; title: string;
description?: string; description?: string;
} }

View file

@ -8,6 +8,8 @@
interface Props { interface Props {
test: Test; test: Test;
nbfetch: number;
nextfetch: number;
fetching?: boolean; fetching?: boolean;
} }

View file

@ -5,8 +5,8 @@
interface Props { interface Props {
spamassassin: SpamAssassinResult; spamassassin: SpamAssassinResult;
spamGrade: string; spamGrade?: string;
spamScore: number; spamScore?: number;
} }
let { spamassassin, spamGrade, spamScore }: Props = $props(); let { spamassassin, spamGrade, spamScore }: Props = $props();

View file

@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { SPFRecord } from "$lib/api/types.gen"; import type { SpfRecord } from "$lib/api/types.gen";
interface Props { interface Props {
spfRecords?: SPFRecord[]; spfRecords?: SpfRecord[];
} }
let { spfRecords }: Props = $props(); let { spfRecords }: Props = $props();
// Compute overall validity // Compute overall validity
const spfIsValid = $derived( const spfIsValid = $derived(spfRecords?.reduce((acc, r) => acc && r.valid, true) ?? false);
spfRecords?.reduce((acc, r) => acc && r.valid, true) ?? false
);
const spfCanBeImprove = $derived( const spfCanBeImprove = $derived(
spfRecords.length > 0 && spfRecords.filter((r) => !r.record.includes(" redirect="))[0]?.all_qualifier != "-" spfRecords &&
spfRecords.length > 0 &&
spfRecords.filter((r) => !r.record?.includes(" redirect="))[0]?.all_qualifier != "-",
); );
</script> </script>
@ -58,13 +58,13 @@
{#if spf.all_qualifier} {#if spf.all_qualifier}
<div class="mb-2"> <div class="mb-2">
<strong>All Mechanism Policy:</strong> <strong>All Mechanism Policy:</strong>
{#if spf.all_qualifier === '-'} {#if spf.all_qualifier === "-"}
<span class="badge bg-success">Strict (-all)</span> <span class="badge bg-success">Strict (-all)</span>
{:else if spf.all_qualifier === '~'} {:else if spf.all_qualifier === "~"}
<span class="badge bg-warning">Softfail (~all)</span> <span class="badge bg-warning">Softfail (~all)</span>
{:else if spf.all_qualifier === '+'} {:else if spf.all_qualifier === "+"}
<span class="badge bg-danger">Pass (+all)</span> <span class="badge bg-danger">Pass (+all)</span>
{:else if spf.all_qualifier === '?'} {:else if spf.all_qualifier === "?"}
<span class="badge bg-warning">Neutral (?all)</span> <span class="badge bg-warning">Neutral (?all)</span>
{/if} {/if}
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))} {#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))}

View file

@ -5,8 +5,10 @@
interface TextSegment { interface TextSegment {
text: string; text: string;
highlight?: { highlight?: {
color: "good" | "warning" | "danger"; color?: "good" | "warning" | "danger";
bold?: boolean; bold?: boolean;
emphasis?: boolean;
monospace?: boolean;
}; };
link?: string; link?: string;
} }
@ -22,19 +24,19 @@
// Email sender information // Email sender information
const mailFrom = report.header_analysis?.headers?.from?.value || "an unknown sender"; const mailFrom = report.header_analysis?.headers?.from?.value || "an unknown sender";
const hasDkim = report.authentication?.dkim && report.authentication.dkim.length > 0; const hasDkim = report.authentication?.dkim && report.authentication?.dkim.length > 0;
const dkimPassed = hasDkim && report.authentication.dkim.some(d => d.result === "pass"); const dkimPassed = hasDkim && report.authentication?.dkim?.some((d) => d.result === "pass");
segments.push({ text: "Received a " }); segments.push({ text: "Received a " });
segments.push({ segments.push({
text: dkimPassed ? "DKIM-signed" : "non-DKIM-signed", text: dkimPassed ? "DKIM-signed" : "non-DKIM-signed",
highlight: { color: dkimPassed ? "good" : "danger", bold: true }, highlight: { color: dkimPassed ? "good" : "danger", bold: true },
link: "#authentication-dkim" link: "#authentication-dkim",
}); });
segments.push({ text: " email from " }); segments.push({ text: " email from " });
segments.push({ segments.push({
text: mailFrom, text: mailFrom,
highlight: { emphasis: true } highlight: { emphasis: true },
}); });
// Server information and hops // Server information and hops
@ -47,12 +49,12 @@
segments.push({ segments.push({
text: serverName, text: serverName,
highlight: { monospace: true }, highlight: { monospace: true },
link: "#header-details" link: "#header-details",
}); });
segments.push({ text: " after " }); segments.push({ text: " after " });
segments.push({ segments.push({
text: `${hopCount-1} hop${hopCount-1 !== 1 ? "s" : ""}`, text: `${hopCount - 1} hop${hopCount - 1 !== 1 ? "s" : ""}`,
link: "#email-path" link: "#email-path",
}); });
} }
@ -65,22 +67,25 @@
segments.push({ segments.push({
text: "authenticated", text: "authenticated",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#authentication-details" link: "#authentication-details",
}); });
segments.push({ text: " to send email on behalf of " }); segments.push({ text: " to send email on behalf of " });
segments.push({ text: report.header_analysis?.domain_alignment?.from_domain, highlight: {monospace: true} }); segments.push({
text: report.header_analysis?.domain_alignment?.from_domain || "unknown domain",
highlight: { monospace: true },
});
} else if (spfResult && spfResult !== "none") { } else if (spfResult && spfResult !== "none") {
segments.push({ segments.push({
text: "not authenticated", text: "not authenticated",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#authentication-spf" link: "#authentication-spf",
}); });
segments.push({ text: " (failed authentication checks)" }); segments.push({ text: " (failed authentication checks)" });
} else { } else {
segments.push({ segments.push({
text: "not authenticated", text: "not authenticated",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#authentication-details" link: "#authentication-details",
}); });
segments.push({ text: " (lacks proper authentication)" }); segments.push({ text: " (lacks proper authentication)" });
} }
@ -92,21 +97,23 @@
segments.push({ segments.push({
text: "failed", text: "failed",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#authentication-spf" link: "#authentication-spf",
});
segments.push({
text: ", the sending server is not authorized to send mail for this domain",
}); });
segments.push({ text: ", the sending server is not authorized to send mail for this domain" });
} else if (spfResult === "softfail") { } else if (spfResult === "softfail") {
segments.push({ segments.push({
text: "soft-failed", text: "soft-failed",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#authentication-spf" link: "#authentication-spf",
}); });
segments.push({ text: ", the sending server may not be authorized" }); segments.push({ text: ", the sending server may not be authorized" });
} else if (spfResult === "temperror" || spfResult === "permerror") { } else if (spfResult === "temperror" || spfResult === "permerror") {
segments.push({ segments.push({
text: "encountered an error", text: "encountered an error",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#authentication-spf" link: "#authentication-spf",
}); });
segments.push({ text: ", check your SPF record configuration" }); segments.push({ text: ", check your SPF record configuration" });
} else if (spfResult === "none") { } else if (spfResult === "none") {
@ -114,9 +121,11 @@
segments.push({ segments.push({
text: "no SPF record", text: "no SPF record",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#dns-spf" link: "#dns-spf",
});
segments.push({
text: ", you should add one to specify which servers can send email on your behalf",
}); });
segments.push({ text: ", you should add one to specify which servers can send email on your behalf" });
} }
} }
@ -129,13 +138,13 @@
segments.push({ segments.push({
text: "good", text: "good",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#dns-ptr" link: "#dns-ptr",
}); });
} else if (iprevResult.result === "fail") { } else if (iprevResult.result === "fail") {
segments.push({ segments.push({
text: "failed", text: "failed",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#dns-ptr" link: "#dns-ptr",
}); });
segments.push({ text: " to pass the test" }); segments.push({ text: " to pass the test" });
} else { } else {
@ -143,7 +152,7 @@
segments.push({ segments.push({
text: iprevResult.result, text: iprevResult.result,
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#dns-ptr" link: "#dns-ptr",
}); });
} }
} }
@ -152,20 +161,20 @@
const blacklists = report.blacklists; const blacklists = report.blacklists;
if (blacklists && Object.keys(blacklists).length > 0) { if (blacklists && Object.keys(blacklists).length > 0) {
const allChecks = Object.values(blacklists).flat(); const allChecks = Object.values(blacklists).flat();
const listedCount = allChecks.filter(check => check.listed).length; const listedCount = allChecks.filter((check) => check.listed).length;
segments.push({ text: ". Your server is " }); segments.push({ text: ". Your server is " });
if (listedCount > 0) { if (listedCount > 0) {
segments.push({ segments.push({
text: `blacklisted on ${listedCount} list${listedCount !== 1 ? "s" : ""}`, text: `blacklisted on ${listedCount} list${listedCount !== 1 ? "s" : ""}`,
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#rbl-details" link: "#rbl-details",
}); });
} else { } else {
segments.push({ segments.push({
text: "not blacklisted", text: "not blacklisted",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#rbl-details" link: "#rbl-details",
}); });
} }
} }
@ -178,7 +187,7 @@
segments.push({ segments.push({
text: "good", text: "good",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#domain-alignment" link: "#domain-alignment",
}); });
if (!domainAlignment.aligned) { if (!domainAlignment.aligned) {
segments.push({ text: " using organizational domain" }); segments.push({ text: " using organizational domain" });
@ -187,17 +196,22 @@
segments.push({ segments.push({
text: "misaligned", text: "misaligned",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#domain-alignment" link: "#domain-alignment",
}); });
segments.push({ text: ": " }); segments.push({ text: ": " });
segments.push({ text: "Return-Path", highlight: { monospace: true } }); segments.push({ text: "Return-Path", highlight: { monospace: true } });
segments.push({ text: " is set to an address of " }); 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:
report.header_analysis?.domain_alignment?.return_path_domain ||
"unknown domain",
highlight: { monospace: true },
});
segments.push({ text: ", you should " }); segments.push({ text: ", you should " });
segments.push({ segments.push({
text: "update it", text: "update it",
highlight: { bold: true }, highlight: { bold: true },
link: "#domain-alignment" link: "#domain-alignment",
}); });
} }
} }
@ -210,25 +224,28 @@
segments.push({ segments.push({
text: "don't have", text: "don't have",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#dns-dmarc" link: "#dns-dmarc",
}); });
segments.push({ text: " a DMARC record, " }); segments.push({ text: " a DMARC record, " });
segments.push({ text: "consider adding at least a record with the '", highlight: { bold : true } }); 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: "none", highlight: { monospace: true, bold: true } });
segments.push({ text: "' policy", highlight: { bold : true } }); segments.push({ text: "' policy", highlight: { bold: true } });
} else if (!dmarcRecord.valid) { } else if (!dmarcRecord.valid) {
segments.push({ text: ". Your DMARC record has " }); segments.push({ text: ". Your DMARC record has " });
segments.push({ segments.push({
text: "issues", text: "issues",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#dns-dmarc" link: "#dns-dmarc",
}); });
} else if (dmarcRecord.policy === "none") { } else if (dmarcRecord.policy === "none") {
segments.push({ text: ". Your DMARC policy is " }); segments.push({ text: ". Your DMARC policy is " });
segments.push({ segments.push({
text: "set to 'none'", text: "set to 'none'",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#dns-dmarc" link: "#dns-dmarc",
}); });
segments.push({ text: ", which provides monitoring but no protection" }); segments.push({ text: ", which provides monitoring but no protection" });
} else if (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject") { } else if (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject") {
@ -236,7 +253,7 @@
segments.push({ segments.push({
text: dmarcRecord.policy, text: dmarcRecord.policy,
highlight: { color: "good", bold: true, monospace: true }, highlight: { color: "good", bold: true, monospace: true },
link: "#dns-dmarc" link: "#dns-dmarc",
}); });
segments.push({ text: "'" }); segments.push({ text: "'" });
if (dmarcRecord.policy === "reject") { if (dmarcRecord.policy === "reject") {
@ -247,17 +264,17 @@
segments.push({ text: "'" }); segments.push({ text: "'" });
} }
} }
} else if (dmarcResult && dmarcResult.result === "fail") { } else if (dmarcResult === "fail") {
segments.push({ text: ". DMARC check " }); segments.push({ text: ". DMARC check " });
segments.push({ segments.push({
text: "failed", text: "failed",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#authentication-dmarc" link: "#authentication-dmarc",
}); });
} }
// BIMI // BIMI
if (dmarcRecord.valid && dmarcRecord.policy != "none") { if (dmarcRecord && dmarcRecord.valid && dmarcRecord.policy != "none") {
const bimiResult = report.authentication?.bimi; const bimiResult = report.authentication?.bimi;
const bimiRecord = report.dns_results?.bimi_record; const bimiRecord = report.dns_results?.bimi_record;
if (bimiRecord?.valid) { if (bimiRecord?.valid) {
@ -268,7 +285,7 @@
link: "#dns-bimi" link: "#dns-bimi"
}); });
segments.push({ text: " for brand indicator display" }); segments.push({ text: " for brand indicator display" });
} else if (bimiResult && bimiResult.details.indexOf("(No BIMI records found)") >= 0) { } else if (bimiResult && bimiResult.details && bimiResult.details.indexOf("(No BIMI records found)") >= 0) {
segments.push({ text: ". Your domain has no " }); segments.push({ text: ". Your domain has no " });
segments.push({ segments.push({
text: "BIMI record", text: "BIMI record",
@ -293,19 +310,21 @@
segments.push({ text: ". " }); segments.push({ text: ". " });
segments.push({ segments.push({
text: "ARC chain validation", text: "ARC chain validation",
link: "#authentication-arc" link: "#authentication-arc",
}); });
segments.push({ text: " " }); segments.push({ text: " " });
if (arcResult.chain_valid) { if (arcResult.chain_valid) {
segments.push({ segments.push({
text: "passed", text: "passed",
highlight: { color: "good", bold: true } highlight: { color: "good", bold: true },
});
segments.push({
text: ` with ${arcResult.chain_length} set${arcResult.chain_length !== 1 ? "s" : ""}, indicating proper email forwarding`,
}); });
segments.push({ text: ` with ${arcResult.chain_length} set${arcResult.chain_length !== 1 ? "s" : ""}, indicating proper email forwarding` });
} else { } else {
segments.push({ segments.push({
text: "failed", text: "failed",
highlight: { color: "danger", bold: true } highlight: { color: "danger", bold: true },
}); });
segments.push({ text: ", which may indicate issues with email forwarding" }); segments.push({ text: ", which may indicate issues with email forwarding" });
} }
@ -316,20 +335,25 @@
const listUnsubscribe = headers?.["list-unsubscribe"]; const listUnsubscribe = headers?.["list-unsubscribe"];
const listUnsubscribePost = headers?.["list-unsubscribe-post"]; const listUnsubscribePost = headers?.["list-unsubscribe-post"];
const hasNewsletterHeaders = (listUnsubscribe?.importance === "newsletter" && listUnsubscribe?.present) || const hasNewsletterHeaders =
(listUnsubscribePost?.importance === "newsletter" && listUnsubscribePost?.present); (listUnsubscribe?.importance === "newsletter" && listUnsubscribe?.present) ||
(listUnsubscribePost?.importance === "newsletter" && listUnsubscribePost?.present);
if (!hasNewsletterHeaders && (listUnsubscribe?.importance === "newsletter" || listUnsubscribePost?.importance === "newsletter")) { if (
!hasNewsletterHeaders &&
(listUnsubscribe?.importance === "newsletter" ||
listUnsubscribePost?.importance === "newsletter")
) {
segments.push({ text: ". This email is " }); segments.push({ text: ". This email is " });
segments.push({ segments.push({
text: "missing unsubscribe headers", text: "missing unsubscribe headers",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#header-details" link: "#header-details",
}); });
segments.push({ text: " and is " }); segments.push({ text: " and is " });
segments.push({ segments.push({
text: "not suitable for marketing campaigns", text: "not suitable for marketing campaigns",
highlight: { bold: true } highlight: { bold: true },
}); });
} }
@ -344,7 +368,7 @@
segments.push({ segments.push({
text: "flagged as spam", text: "flagged as spam",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#spam-details" link: "#spam-details",
}); });
segments.push({ text: " and needs review" }); segments.push({ text: " and needs review" });
} else if (contentScore < 50) { } else if (contentScore < 50) {
@ -352,49 +376,55 @@
segments.push({ segments.push({
text: "needs improvement", text: "needs improvement",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#content-details" link: "#content-details",
}); });
} else if (contentScore >= 100 && spamScore >= 100) { } else if (contentScore >= 100 && spamScore >= 100) {
segments.push({ text: "Content " }); segments.push({ text: "Content " });
segments.push({ segments.push({
text: "looks great", text: "looks great",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#content-details" link: "#content-details",
}); });
} else if (spamScore < 50) { } else if (spamScore < 50) {
segments.push({ text: "Your " }); segments.push({ text: "Your " });
segments.push({ segments.push({
text: "spam score", text: "spam score",
highlight: { color: "danger", bold: true }, highlight: { color: "danger", bold: true },
link: "#spam-details" link: "#spam-details",
}); });
segments.push({ text: " is low" }); segments.push({ text: " is low" });
if (report.spamassassin.tests.includes("EMPTY_MESSAGE")) { if (report.spamassassin?.tests?.includes("EMPTY_MESSAGE")) {
segments.push({ text: " (you sent an empty message, which can cause this issue, retry with some real content)", highlight: { bold: true } }); segments.push({
text: " (you sent an empty message, which can cause this issue, retry with some real content)",
highlight: { bold: true },
});
} }
} else if (spamScore < 90) { } else if (spamScore < 90) {
segments.push({ text: "Pay attention to your " }); segments.push({ text: "Pay attention to your " });
segments.push({ segments.push({
text: "spam score", text: "spam score",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#spam-details" link: "#spam-details",
}); });
if (report.spamassassin.tests.includes("EMPTY_MESSAGE")) { if (report.spamassassin?.tests?.includes("EMPTY_MESSAGE")) {
segments.push({ text: " (you sent an empty message, which can cause this issue, retry with some real content)", highlight: { bold: true } }); segments.push({
text: " (you sent an empty message, which can cause this issue, retry with some real content)",
highlight: { bold: true },
});
} }
} else if (contentScore >= 80) { } else if (contentScore >= 80) {
segments.push({ text: "Content " }); segments.push({ text: "Content " });
segments.push({ segments.push({
text: "looks good", text: "looks good",
highlight: { color: "good", bold: true }, highlight: { color: "good", bold: true },
link: "#content-details" link: "#content-details",
}); });
} else { } else {
segments.push({ text: "Content " }); segments.push({ text: "Content " });
segments.push({ segments.push({
text: "should be reviewed", text: "should be reviewed",
highlight: { color: "warning", bold: true }, highlight: { color: "warning", bold: true },
link: "#content-details" link: "#content-details",
}); });
} }
@ -403,7 +433,7 @@
return segments; return segments;
} }
function getColorClass(color: "good" | "warning" | "danger"): string { function getColorClass(color?: "good" | "warning" | "danger"): string {
switch (color) { switch (color) {
case "good": case "good":
return "text-success"; return "text-success";
@ -411,28 +441,14 @@
return "text-warning"; return "text-warning";
case "danger": case "danger":
return "text-danger"; return "text-danger";
default:
return "";
} }
} }
const summarySegments = $derived(buildSummary()); const summarySegments = $derived(buildSummary());
</script> </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 shadow-sm border-0 mb-4">
<div class="card-body p-4"> <div class="card-body p-4">
<h5 class="card-title mb-3"> <h5 class="card-title mb-3">
@ -460,3 +476,19 @@
</p> </p>
</div> </div>
</div> </div>
<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>

View file

@ -7,8 +7,8 @@ export class NotAuthorizedError extends Error {
} }
} }
async function customFetch(url: string, init: RequestInit): Promise<Response> { async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const response = await fetch(url, init); const response = await fetch(input, init);
if (response.status === 400) { if (response.status === 400) {
const json = await response.json(); const json = await response.json();

View file

@ -38,7 +38,12 @@
</h3> </h3>
<ul class="footer-links"> <ul class="footer-links">
<li><a href="/#features">Features</a></li> <li><a href="/#features">Features</a></li>
<li><a href="#">Download</a></li> <li>
<a
href="https://github.com/happyDomain/happydeliver/releases"
target="_blank">Download</a
>
</li>
<li> <li>
<a href="https://github.com/happyDomain/happydeliver/" target="_blank"> <a href="https://github.com/happyDomain/happydeliver/" target="_blank">
GitHub GitHub
@ -73,16 +78,32 @@
class="d-flex flex-wrap justify-content-between footer-links" class="d-flex flex-wrap justify-content-between footer-links"
style="gap: .5em; font-size: 2em" style="gap: .5em; font-size: 2em"
> >
<a href="https://framagit.org/happyDomain/happydeliver" target="_blank"> <a
href="https://framagit.org/happyDomain/happydeliver"
target="_blank"
aria-label="Visit our GitLab repository"
>
<i class="bi bi-gitlab"></i> <i class="bi bi-gitlab"></i>
</a> </a>
<a href="https://github.com/happyDomain/happydeliver" target="_blank"> <a
href="https://github.com/happyDomain/happydeliver"
target="_blank"
aria-label="Visit our GitHub repository"
>
<i class="bi bi-github"></i> <i class="bi bi-github"></i>
</a> </a>
<a href="https://feedback.happydomain.org/" target="_blank"> <a
href="https://feedback.happydomain.org/"
target="_blank"
aria-label="Share your feedback"
>
<i class="bi bi-lightbulb-fill"></i> <i class="bi bi-lightbulb-fill"></i>
</a> </a>
<a href="https://floss.social/@happyDomain" target="_blank"> <a
href="https://floss.social/@happyDomain"
target="_blank"
aria-label="Follow us on Mastodon"
>
<i class="bi bi-mastodon"></i> <i class="bi bi-mastodon"></i>
</a> </a>
</div> </div>

View file

@ -10,10 +10,11 @@ export const load: Load = async ({}) => {
try { try {
response = await apiCreateTest(); response = await apiCreateTest();
} catch (err) { } catch (err) {
error(err.response.status, err.message); const errorObj = err as { response?: { status?: number }; message?: string };
error(errorObj.response?.status || 500, errorObj.message || "Unknown error");
} }
if (response.response.ok) { if (response.response.ok && response.data) {
redirect(302, `/test/${response.data.id}`); redirect(302, `/test/${response.data.id}`);
} else { } else {
error(response.response.status, response.error); error(response.response.status, response.error);

View file

@ -28,6 +28,8 @@
let fetching = $state(false); let fetching = $state(false);
async function fetchTest() { async function fetchTest() {
if (!testId) return;
if (nbfetch > 0) { if (nbfetch > 0) {
nextfetch = Math.max(nextfetch, Math.floor(3 + nbfetch * 0.5)); nextfetch = Math.max(nextfetch, Math.floor(3 + nbfetch * 0.5));
} }
@ -89,8 +91,8 @@
} }
} }
let lastTestId = null; let lastTestId: string | null = null;
function testChange(newTestId) { function testChange(newTestId: string) {
if (lastTestId != newTestId) { if (lastTestId != newTestId) {
lastTestId = newTestId; lastTestId = newTestId;
test = null; test = null;
@ -100,7 +102,10 @@
} }
$effect(() => { $effect(() => {
testChange(page.params.test); const newTestId = page.params.test;
if (newTestId) {
testChange(newTestId);
}
}) })
onDestroy(() => { onDestroy(() => {
@ -128,9 +133,9 @@
function handleExportJSON() { function handleExportJSON() {
const dataStr = JSON.stringify(report, null, 2); const dataStr = JSON.stringify(report, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' }); const dataBlob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(dataBlob); const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a'); const link = document.createElement("a");
link.href = url; link.href = url;
link.download = `report-${testId}.json`; link.download = `report-${testId}.json`;
link.click(); link.click();
@ -140,7 +145,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{report ? `Test of ${report.dns_results.from_domain} ${report.test_id.slice(0, 7)}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver</title> <title>{report ? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ''} ${report.test_id?.slice(0, 7) || ''}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver</title>
</svelte:head> </svelte:head>
<div class="container py-5"> <div class="container py-5">
@ -283,11 +288,11 @@
<div class="row mb-4" id="header"> <div class="row mb-4" id="header">
<div class="col-12"> <div class="col-12">
<HeaderAnalysisCard <HeaderAnalysisCard
dmarcRecord={report.dns_results.dmarc_record} dmarcRecord={report.dns_results?.dmarc_record}
headerAnalysis={report.header_analysis} headerAnalysis={report.header_analysis}
headerGrade={report.summary?.header_grade} headerGrade={report.summary?.header_grade}
headerScore={report.summary?.header_score} headerScore={report.summary?.header_score}
xAlignedFrom={report.authentication.x_aligned_from} xAlignedFrom={report.authentication?.x_aligned_from}
/> />
</div> </div>
</div> </div>
@ -348,23 +353,6 @@
} }
} }
.category-section {
margin-bottom: 2rem;
}
.category-title {
font-size: 1.25rem;
font-weight: 600;
color: #495057;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e9ecef;
}
.category-score {
font-size: 1rem;
font-weight: 700;
}
.menu-container { .menu-container {
position: relative; position: relative;
} }