Improve test display in some circonstancies

This commit is contained in:
nemunaire 2025-10-24 10:22:16 +07:00
commit 7ed347c86e
9 changed files with 118 additions and 77 deletions

View file

@ -751,9 +751,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s
brokenLinks++
}
}
if brokenLinks == 0 {
score += 20
}
score += 20 * brokenLinks / len(results.Links)
// Too much links, 10 points penalty
if len(results.Links) > 30 {
score -= 10
@ -771,11 +769,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s
noAltCount++
}
}
if noAltCount == 0 {
score += 15
} else if noAltCount < len(results.Images) {
score += 7
}
score += 15 * noAltCount / len(results.Images)
} else {
// No images is Ok
score += 15
@ -795,20 +789,12 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s
// Penalize suspicious URLs (deduct up to 5 points)
if len(results.SuspiciousURLs) > 0 {
penalty := len(results.SuspiciousURLs)
if penalty > 5.0 {
penalty = 5
}
score -= penalty
score -= min(len(results.SuspiciousURLs), 5)
}
// Penalize harmful HTML tags (deduct 20 points per harmful tag, max 40 points)
if len(results.HarmfullIssues) > 0 {
penalty := len(results.HarmfullIssues) * 20
if penalty > 40 {
penalty = 40
}
score -= penalty
score -= min(len(results.HarmfullIssues)*20, 40)
}
// Ensure score is between 0 and 100

View file

@ -12,7 +12,7 @@
let { authentication, authenticationGrade, authenticationScore, dnsResults }: Props = $props();
function getAuthResultClass(result: string): string {
function getAuthResultClass(result: string, noneIsFail: boolean): string {
switch (result) {
case "pass":
return "text-success";
@ -22,12 +22,14 @@
case "softfail":
case "neutral":
return "text-warning";
case "none":
return noneIsFail ? "text-danger" : "text-muted";
default:
return "text-muted";
}
}
function getAuthResultIcon(result: string): string {
function getAuthResultIcon(result: string, noneIsFail: boolean): string {
switch (result) {
case "pass":
return "bi-check-circle-fill";
@ -38,6 +40,8 @@
return "bi-exclamation-circle-fill";
case "missing":
return "bi-dash-circle-fill";
case "none":
return noneIsFail ? "bi-x-circle-fill" : "bi-question-circle";
default:
return "bi-question-circle";
}
@ -77,10 +81,10 @@
{#if authentication.iprev}
<div class="list-group-item" id="authentication-iprev">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.iprev.result)} {getAuthResultClass(authentication.iprev.result)} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon(authentication.iprev.result, true)} {getAuthResultClass(authentication.iprev.result, true)} me-2 fs-5"></i>
<div>
<strong>IP Reverse DNS</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.iprev.result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.iprev.result, true)}">
{authentication.iprev.result}
</span>
{#if authentication.iprev.ip}
@ -107,10 +111,10 @@
<div class="list-group-item">
<div class="d-flex align-items-start" id="authentication-spf">
{#if authentication.spf}
<i class="bi {getAuthResultIcon(authentication.spf.result)} {getAuthResultClass(authentication.spf.result)} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon(authentication.spf.result, true)} {getAuthResultClass(authentication.spf.result, true)} me-2 fs-5"></i>
<div>
<strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.spf.result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.spf.result, true)}">
{authentication.spf.result}
</span>
{#if authentication.spf.domain}
@ -140,10 +144,10 @@
<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>
<i class="bi {getAuthResultIcon(authentication.dkim[0].result, true)} {getAuthResultClass(authentication.dkim[0].result, true)} me-2 fs-5"></i>
<div>
<strong>DKIM</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dkim[0].result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dkim[0].result, true)}">
{authentication.dkim[0].result}
</span>
{#if authentication.dkim[0].domain}
@ -179,10 +183,10 @@
<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>
<i class="bi {getAuthResultIcon(authentication.dmarc.result, true)} {getAuthResultClass(authentication.dmarc.result, true)} me-2 fs-5"></i>
<div>
<strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dmarc.result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dmarc.result, true)}">
{authentication.dmarc.result}
</span>
{#if authentication.dmarc.domain}
@ -205,11 +209,13 @@
</span>
</div>
{/snippet}
{#if authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
{@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{#if authentication.dmarc.result != "none"}
{#if authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
{@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{/if}
{/if}
{#if authentication.dmarc.details}
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.dmarc.details}</pre>

View file

@ -71,7 +71,7 @@
{#if contentAnalysis.html_issues && contentAnalysis.html_issues.length > 0}
<div class="mt-3">
<h6>Content Issues</h6>
<h5>Content Issues</h5>
{#each contentAnalysis.html_issues as issue}
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
<div class="d-flex justify-content-between align-items-start">
@ -97,7 +97,7 @@
{#if contentAnalysis.links && contentAnalysis.links.length > 0}
<div class="mt-3">
<h6>Links ({contentAnalysis.links.length})</h6>
<h5>Links ({contentAnalysis.links.length})</h5>
<div class="table-responsive">
<table class="table table-sm">
<thead>
@ -132,7 +132,7 @@
{#if contentAnalysis.images && contentAnalysis.images.length > 0}
<div class="mt-3">
<h6>Images ({contentAnalysis.images.length})</h6>
<h5>Images ({contentAnalysis.images.length})</h5>
<div class="table-responsive">
<table class="table table-sm">
<thead>

View file

@ -8,30 +8,31 @@
let { dkimRecords }: Props = $props();
// Compute overall validity
const dkimIsValid = $derived(
dkimRecords?.reduce((acc, r) => acc && r.valid, true) ?? false
);
const dkimIsValid = $derived(dkimRecords?.reduce((acc, r) => acc && r.valid, true) ?? false);
</script>
{#if dkimRecords && dkimRecords.length > 0}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i
class="bi"
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">
<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">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i
class="bi"
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">
<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">
{#if dkimRecords && dkimRecords.length > 0}
{#each dkimRecords as dkim}
<div class="list-group-item">
<div class="mb-2">
@ -48,17 +49,26 @@
</div>
{#if dkim.record}
<div class="mb-2">
<strong>Record:</strong><br>
<code class="d-block mt-1 text-break small text-truncate">{dkim.record}</code>
<strong>Record:</strong><br />
<code class="d-block mt-1 text-break small text-truncate"
>{dkim.record}</code
>
</div>
{/if}
{#if dkim.error}
<div class="text-danger">
<strong>Error:</strong> {dkim.error}
<strong>Error:</strong>
{dkim.error}
</div>
{/if}
</div>
{/each}
</div>
{:else}
<div class="list-group-item text-muted">
<i class="bi bi-exclamation-octagon me-2"></i>
No DKIM signatures found in this email. DKIM provides cryptographic authentication and
helps avoid spoofing, thus improving deliverability.
</div>
{/if}
</div>
{/if}
</div>

View file

@ -63,8 +63,8 @@
<!-- Reverse IP Section -->
{#if receivedChain && receivedChain.length > 0}
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0">
Received by: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
<h4 class="mb-0 text-truncate">
Received from: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
</h4>
</div>
{/if}
@ -84,7 +84,7 @@
<!-- Return-Path Domain Section -->
<div class="mb-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<h4 class="mb-0">
<h4 class="mb-0 text-truncate">
Return-Path Domain: <code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
</h4>
{#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)}
@ -116,7 +116,7 @@
<!-- From Domain Section -->
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0">
<h4 class="mb-0 text-truncate">
From Domain: <code>{dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}

View file

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

View file

@ -42,7 +42,7 @@
{#if summary}
<div class="row g-3 text-start">
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#dns-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.dns_grade} score={summary.dns_score} />
@ -50,7 +50,7 @@
</div>
</a>
</div>
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#authentication-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.authentication_grade} score={summary.authentication_score} />
@ -58,7 +58,7 @@
</div>
</a>
</div>
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#rbl-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.blacklist_grade} score={summary.blacklist_score} />
@ -66,7 +66,7 @@
</div>
</a>
</div>
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#header-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.header_grade} score={summary.header_score} />
@ -74,7 +74,7 @@
</div>
</a>
</div>
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#spam-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.spam_grade} score={summary.spam_score} />
@ -82,7 +82,7 @@
</div>
</a>
</div>
<div class="col-md-6 col-lg">
<div class="col-sm-6 col-md-4 col-lg">
<a href="#content-details" class="text-decoration-none">
<div class="p-2 bg-light rounded text-center summary-card">
<GradeDisplay grade={summary.content_grade} score={summary.content_score} />

View file

@ -85,6 +85,41 @@
segments.push({ text: " (lacks proper authentication)" });
}
// SPF specific issues
if (spfResult && spfResult !== "pass") {
segments.push({ text: ". SPF check " });
if (spfResult === "fail") {
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#authentication-spf"
});
segments.push({ text: ", the sending server is not authorized to send mail for this domain" });
} else if (spfResult === "softfail") {
segments.push({
text: "soft-failed",
highlight: { color: "warning", bold: true },
link: "#authentication-spf"
});
segments.push({ text: ", the sending server may not be authorized" });
} else if (spfResult === "temperror" || spfResult === "permerror") {
segments.push({
text: "encountered an error",
highlight: { color: "warning", bold: true },
link: "#authentication-spf"
});
segments.push({ text: ", check your SPF record configuration" });
} else if (spfResult === "none") {
segments.push({ text: "Your domain has " });
segments.push({
text: "no SPF record",
highlight: { color: "danger", bold: true },
link: "#dns-spf"
});
segments.push({ text: ", you should add one to specify which servers can send email on your behalf" });
}
}
// IP Reverse DNS (iprev) check
const iprevResult = report.authentication?.iprev;
if (iprevResult) {
@ -207,7 +242,9 @@
if (dmarcRecord.policy === "reject") {
segments.push({ text: ", which is great" });
} else {
segments.push({ text: ", consider switching to reject" });
segments.push({ text: ", consider switching to '" });
segments.push({ text: "reject", highlight: { monospace: true, bold: true } });
segments.push({ text: "'" });
}
}
} else if (dmarcResult && dmarcResult.result === "fail") {
@ -238,6 +275,8 @@
highlight: { color: "warning", bold: true },
link: "#dns-bimi"
});
segments.push({ text: ", you could " });
segments.push({ text: "add a record to decline participation", highlight: { bold: true } });
} else if (bimiResult || bimiRecord) {
segments.push({ text: ". Your domain has " });
segments.push({
@ -395,7 +434,7 @@
{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 🔽
Overall, your email received a grade <GradeDisplay grade={report.grade} score={report.score} size="inline" />{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: you could have delivery issues with common providers.{:else if report.grade == "F"}: it will most likely be rejected by most providers.{:else}!{/if} Check the details below 🔽
</p>
</div>
</div>

View file

@ -102,7 +102,7 @@
</script>
<svelte:head>
<title>{test ? `Test ${test.id.slice(0, 7)} - happyDeliver` : "Loading..."}</title>
<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>
</svelte:head>
<div class="container py-5">