Improve test display in some circonstancies
This commit is contained in:
parent
4bbba66a81
commit
7ed347c86e
9 changed files with 118 additions and 77 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue