Add domain only tests

This commit is contained in:
nemunaire 2025-10-31 10:10:58 +07:00
commit 718b624fb8
10 changed files with 663 additions and 68 deletions

View file

@ -17,9 +17,10 @@
dnsGrade?: string;
dnsScore?: number;
receivedChain?: ReceivedHop[];
domainOnly?: boolean; // If true, only shows domain-level DNS records (no PTR, no DKIM, simplified view)
}
let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain }: Props = $props();
let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain, domainOnly = false }: Props = $props();
// Extract sender IP from first hop
const senderIp = $derived(
@ -61,88 +62,94 @@
</div>
{/if}
<!-- Reverse IP Section -->
{#if receivedChain && receivedChain.length > 0}
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0 text-truncate">
Received from: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
</h4>
</div>
{/if}
{#if !domainOnly}
<!-- Reverse IP Section -->
{#if receivedChain && receivedChain.length > 0}
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0 text-truncate">
Received from: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
</h4>
</div>
{/if}
<!-- PTR Records Section -->
<PtrRecordsDisplay ptrRecords={dnsResults.ptr_records} {senderIp} />
<!-- PTR Records Section -->
<PtrRecordsDisplay ptrRecords={dnsResults.ptr_records} {senderIp} />
<!-- Forward-Confirmed Reverse DNS -->
<PtrForwardRecordsDisplay
ptrRecords={dnsResults.ptr_records}
ptrForwardRecords={dnsResults.ptr_forward_records}
{senderIp}
/>
<hr class="my-4" />
<!-- Return-Path Domain Section -->
<div class="mb-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<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)}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain</span>
<small>
<i class="bi bi-chevron-right"></i>
<a href="#domain-alignment">See domain alignment</a>
</small>
{:else}
<span class="badge bg-success ms-2">Same as From domain</span>
{/if}
</div>
</div>
<!-- MX Records for Return-Path Domain -->
{#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.rp_mx_records}
title="Mail Exchange Records for Return-Path Domain"
description="These MX records handle bounce messages and non-delivery reports."
<!-- Forward-Confirmed Reverse DNS -->
<PtrForwardRecordsDisplay
ptrRecords={dnsResults.ptr_records}
ptrForwardRecords={dnsResults.ptr_forward_records}
{senderIp}
/>
<hr class="my-4" />
<!-- Return-Path Domain Section -->
<div class="mb-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<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)}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain</span>
<small>
<i class="bi bi-chevron-right"></i>
<a href="#domain-alignment">See domain alignment</a>
</small>
{:else}
<span class="badge bg-success ms-2">Same as From domain</span>
{/if}
</div>
</div>
<!-- MX Records for Return-Path Domain -->
{#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.rp_mx_records}
title="Mail Exchange Records for Return-Path Domain"
description="These MX records handle bounce messages and non-delivery reports."
/>
{/if}
{/if}
<!-- SPF Records (for Return-Path Domain) -->
<SpfRecordsDisplay spfRecords={dnsResults.spf_records} dmarcRecord={dnsResults.dmarc_record} />
<hr class="my-4">
{#if !domainOnly}
<hr class="my-4">
<!-- From Domain Section -->
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0 text-truncate">
From Domain: <code>{dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path domain</span>
{/if}
</div>
<!-- MX Records for From Domain -->
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.from_mx_records}
title="Mail Exchange Records for From Domain"
description="These MX records handle replies to emails sent from this domain."
/>
<!-- From Domain Section -->
<div class="mb-3 d-flex align-items-center gap-2">
<h4 class="mb-0 text-truncate">
From Domain: <code>{dnsResults.from_domain}</code>
</h4>
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path domain</span>
{/if}
</div>
{/if}
<!-- DKIM Records -->
<DkimRecordsDisplay dkimRecords={dnsResults.dkim_records} />
<!-- MX Records for From Domain -->
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
<MxRecordsDisplay
class="mb-4"
mxRecords={dnsResults.from_mx_records}
title="Mail Exchange Records for From Domain"
description="These MX records handle replies to emails sent from this domain."
/>
{/if}
<!-- DMARC Record -->
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
{#if !domainOnly}
<!-- DKIM Records -->
<DkimRecordsDisplay dkimRecords={dnsResults.dkim_records} />
{/if}
<!-- BIMI Record -->
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
<!-- DMARC Record -->
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
<!-- BIMI Record -->
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
{/if}
</div>
</div>

View file

@ -15,3 +15,11 @@ export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
export { default as TinySurvey } from "./TinySurvey.svelte";
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
export { default as GradeDisplay } from "./GradeDisplay.svelte";
export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte";
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte";
export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte";
export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte";
export { default as Logo } from "./Logo.svelte";
export { default as EmailPathCard } from "./EmailPathCard.svelte";

View file

@ -233,6 +233,13 @@
{/if}
</button>
</div>
<div class="text-center mt-4">
<a href="/domain" class="btn btn-secondary btn-lg">
<i class="bi bi-globe me-2"></i>
Or Test Domain Only
</a>
</div>
</div>
</section>

View file

@ -0,0 +1,176 @@
<script lang="ts">
import { goto } from "$app/navigation";
let domain = $state("");
let error = $state<string | null>(null);
function handleSubmit() {
error = null;
if (!domain.trim()) {
error = "Please enter a domain name";
return;
}
// Basic domain validation
const domainPattern = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*$/;
if (!domainPattern.test(domain.trim())) {
error = "Please enter a valid domain name (e.g., example.com)";
return;
}
// Navigate to the domain test page
goto(`/domain/${encodeURIComponent(domain.trim())}`);
}
function handleKeyPress(event: KeyboardEvent) {
if (event.key === "Enter") {
handleSubmit();
}
}
</script>
<svelte:head>
<title>Domain Test - happyDeliver</title>
</svelte:head>
<div class="container py-5">
<div class="row">
<div class="col-lg-8 mx-auto">
<!-- Header -->
<div class="text-center mb-5">
<h1 class="display-4 fw-bold mb-3">
<i class="bi bi-globe me-2"></i>
Test Domain Configuration
</h1>
<p class="lead text-muted">
Check your domain's email DNS records (MX, SPF, DMARC, BIMI) without sending an
email.
</p>
</div>
<!-- Input Form -->
<div class="card shadow-lg border-0 mb-5">
<div class="card-body p-5">
<h2 class="h5 mb-4">Enter Domain Name</h2>
<div class="input-group input-group-lg mb-3">
<span class="input-group-text bg-light">
<i class="bi bi-at"></i>
</span>
<input
type="text"
class="form-control"
placeholder="example.com"
bind:value={domain}
onkeypress={handleKeyPress}
autofocus
/>
<button
class="btn btn-primary px-5"
onclick={handleSubmit}
disabled={!domain.trim()}
>
<i class="bi bi-search me-2"></i>
Analyze
</button>
</div>
{#if error}
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
{error}
</div>
{/if}
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Enter a domain name like "example.com" or "mail.example.org"
</small>
</div>
</div>
<!-- Info Section -->
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card h-100 border-0 bg-light">
<div class="card-body">
<h3 class="h6 mb-3">
<i class="bi bi-check-circle-fill text-success me-2"></i>
What's Checked
</h3>
<ul class="list-unstyled mb-0 small">
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>MX Records</li>
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>SPF Records</li>
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>DMARC Policy</li>
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>BIMI Support</li>
<li class="mb-0">
<i class="bi bi-arrow-right me-2"></i>Disposable Domain Check
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 bg-light">
<div class="card-body">
<h3 class="h6 mb-3">
<i class="bi bi-info-circle-fill text-primary me-2"></i>
Need More?
</h3>
<p class="small mb-2">
For complete email deliverability analysis including:
</p>
<ul class="list-unstyled mb-3 small">
<li class="mb-1">
<i class="bi bi-arrow-right me-2"></i>DKIM Verification
</li>
<li class="mb-1">
<i class="bi bi-arrow-right me-2"></i>Content & Header Analysis
</li>
<li class="mb-1">
<i class="bi bi-arrow-right me-2"></i>Spam Scoring
</li>
<li class="mb-1">
<i class="bi bi-arrow-right me-2"></i>Blacklist Checks
</li>
</ul>
<a href="/" class="btn btn-sm btn-outline-primary">
<i class="bi bi-envelope-plus me-1"></i>
Send Test Email
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.1) !important;
}
.input-group-lg .form-control {
font-size: 1.1rem;
}
.input-group-text {
border-right: none;
}
.input-group .form-control {
border-left: none;
border-right: none;
}
.input-group .form-control:focus {
box-shadow: none;
}
</style>

View file

@ -0,0 +1,181 @@
<script lang="ts">
import { page } from "$app/stores";
import { onMount } from "svelte";
import { testDomain } from "$lib/api";
import type { DomainTestResponse } from "$lib/api/types.gen";
import { GradeDisplay, DnsRecordsCard } from "$lib/components";
import { theme } from "$lib/stores/theme";
let domain = $derived($page.params.domain);
let loading = $state(true);
let error = $state<string | null>(null);
let result = $state<DomainTestResponse | null>(null);
async function analyzeDomain() {
loading = true;
error = null;
result = null;
if (!domain) {
error = "Domain parameter is missing";
loading = false;
return;
}
try {
const response = await testDomain({
body: { domain: domain },
});
if (response.data) {
result = response.data;
} else if (response.error) {
error = response.error.message || "Failed to analyze domain";
}
} catch (err) {
error = err instanceof Error ? err.message : "Failed to analyze domain";
} finally {
loading = false;
}
}
onMount(() => {
analyzeDomain();
});
</script>
<svelte:head>
<title>{domain} - Domain Test - happyDeliver</title>
</svelte:head>
<div class="container py-5">
<div class="row">
<div class="col-lg-10 mx-auto">
<!-- Header -->
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between">
<h1 class="h2 mb-0">
<i class="bi bi-globe me-2"></i>
Domain Analysis
</h1>
<a href="/domain" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>
Test Another Domain
</a>
</div>
</div>
{#if loading}
<!-- Loading State -->
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h3 class="h5">Analyzing {domain}...</h3>
<p class="text-muted mb-0">Checking DNS records and configuration</p>
</div>
</div>
{:else if error}
<!-- Error State -->
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 4rem;"></i>
<h3 class="h4 mt-4">Analysis Failed</h3>
<p class="text-muted mb-4">{error}</p>
<button class="btn btn-primary" onclick={analyzeDomain}>
<i class="bi bi-arrow-clockwise me-2"></i>
Try Again
</button>
</div>
</div>
{:else if result}
<!-- Results -->
<div class="fade-in">
<!-- Score Summary Card -->
<div class="card shadow-sm mb-4">
<div class="card-body p-4">
<div class="row align-items-center">
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
<h2 class="h2 mb-2">
<span class="font-monospace">{result.domain}</span>
</h2>
{#if result.is_disposable}
<div class="alert alert-warning mb-0 d-inline-block">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Disposable Email Provider Detected</strong>
<p class="mb-0 mt-1 small">
This domain is a known temporary/disposable email service.
Emails from this domain may have lower deliverability.
</p>
</div>
{:else}
<p class="text-muted mb-0">Domain Configuration Score</p>
{/if}
</div>
<div class="offset-md-3 col-md-3 text-center">
<div
class="p-2 rounded text-center summary-card"
class:bg-light={$theme === 'light'}
class:bg-secondary={$theme !== 'light'}
>
<GradeDisplay score={result.score} grade={result.grade} />
<small class="text-muted d-block">DNS</small>
</div>
</div>
</div>
</div>
</div>
<!-- DNS Records Card -->
<DnsRecordsCard
dnsResults={result.dns_results}
dnsScore={result.score}
dnsGrade={result.grade}
domainOnly={true}
/>
<!-- Next Steps -->
<div class="card shadow-sm border-primary mt-4">
<div class="card-body">
<h3 class="h5 mb-3">
<i class="bi bi-lightbulb me-2"></i>
Want Complete Email Analysis?
</h3>
<p class="mb-3">
This domain-only test checks DNS configuration. For comprehensive
deliverability testing including DKIM verification, content analysis,
spam scoring, and blacklist checks:
</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-envelope-plus me-2"></i>
Send a Test Email
</a>
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
<style>
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
border: none;
}
</style>