Add domain only tests
This commit is contained in:
parent
099965c1f9
commit
718b624fb8
10 changed files with 663 additions and 68 deletions
|
|
@ -169,6 +169,39 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/domain:
|
||||
post:
|
||||
tags:
|
||||
- tests
|
||||
summary: Test a domain's email configuration
|
||||
description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately.
|
||||
operationId: testDomain
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DomainTestRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Domain test completed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DomainTestResponse'
|
||||
'400':
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/status:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -1112,3 +1145,40 @@ components:
|
|||
details:
|
||||
type: string
|
||||
description: Additional error details
|
||||
|
||||
DomainTestRequest:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
pattern: '^[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])?)*$'
|
||||
description: Domain name to test (e.g., example.com)
|
||||
example: "example.com"
|
||||
|
||||
DomainTestResponse:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
- score
|
||||
- grade
|
||||
- dns_results
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
description: The tested domain name
|
||||
example: "example.com"
|
||||
score:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Overall domain configuration score (0-100)
|
||||
example: 85
|
||||
grade:
|
||||
type: string
|
||||
enum: [A+, A, B, C, D, E, F]
|
||||
description: Letter grade representation of the score
|
||||
example: "A"
|
||||
dns_results:
|
||||
$ref: '#/components/schemas/DNSResults'
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import (
|
|||
// This interface breaks the circular dependency with pkg/analyzer
|
||||
type EmailAnalyzer interface {
|
||||
AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error)
|
||||
AnalyzeDomain(domain string) (dnsResults *DNSResults, score int, grade string)
|
||||
}
|
||||
|
||||
// APIHandler implements the ServerInterface for handling API requests
|
||||
|
|
@ -290,3 +291,53 @@ func (h *APIHandler) GetStatus(c *gin.Context) {
|
|||
Uptime: &uptime,
|
||||
})
|
||||
}
|
||||
|
||||
// TestDomain performs synchronous domain analysis
|
||||
// (POST /domain)
|
||||
func (h *APIHandler) TestDomain(c *gin.Context) {
|
||||
var request DomainTestRequest
|
||||
|
||||
// Bind and validate request
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error{
|
||||
Error: "invalid_request",
|
||||
Message: "Invalid request body",
|
||||
Details: stringPtr(err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Perform domain analysis
|
||||
dnsResults, score, grade := h.analyzer.AnalyzeDomain(request.Domain)
|
||||
|
||||
// Convert grade string to DomainTestResponseGrade enum
|
||||
var responseGrade DomainTestResponseGrade
|
||||
switch grade {
|
||||
case "A+":
|
||||
responseGrade = DomainTestResponseGradeA
|
||||
case "A":
|
||||
responseGrade = DomainTestResponseGradeA1
|
||||
case "B":
|
||||
responseGrade = DomainTestResponseGradeB
|
||||
case "C":
|
||||
responseGrade = DomainTestResponseGradeC
|
||||
case "D":
|
||||
responseGrade = DomainTestResponseGradeD
|
||||
case "E":
|
||||
responseGrade = DomainTestResponseGradeE
|
||||
case "F":
|
||||
responseGrade = DomainTestResponseGradeF
|
||||
default:
|
||||
responseGrade = DomainTestResponseGradeF
|
||||
}
|
||||
|
||||
// Build response
|
||||
response := DomainTestResponse{
|
||||
Domain: request.Domain,
|
||||
Score: score,
|
||||
Grade: responseGrade,
|
||||
DnsResults: *dnsResults,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,3 +108,14 @@ func (a *APIAdapter) AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) ([]byt
|
|||
|
||||
return reportJSON, nil
|
||||
}
|
||||
|
||||
// AnalyzeDomain performs DNS analysis for a domain and returns the results
|
||||
func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) {
|
||||
// Perform DNS analysis
|
||||
dnsResults := a.analyzer.generator.dnsAnalyzer.AnalyzeDomainOnly(domain)
|
||||
|
||||
// Calculate score
|
||||
score, grade := a.analyzer.generator.dnsAnalyzer.CalculateDomainOnlyScore(dnsResults)
|
||||
|
||||
return dnsResults, score, grade
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,70 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic
|
|||
return results
|
||||
}
|
||||
|
||||
// AnalyzeDomainOnly performs DNS validation for a domain without email context
|
||||
// This is useful for checking domain configuration without sending an actual email
|
||||
func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults {
|
||||
results := &api.DNSResults{
|
||||
FromDomain: domain,
|
||||
}
|
||||
|
||||
// Check MX records
|
||||
results.FromMxRecords = d.checkMXRecords(domain)
|
||||
|
||||
// Check SPF records
|
||||
results.SpfRecords = d.checkSPFRecords(domain)
|
||||
|
||||
// Check DMARC record
|
||||
results.DmarcRecord = d.checkDMARCRecord(domain)
|
||||
|
||||
// Check BIMI record with default selector
|
||||
results.BimiRecord = d.checkBIMIRecord(domain, "default")
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// CalculateDomainOnlyScore calculates the DNS score for domain-only tests
|
||||
// Returns a score from 0-100 where higher is better
|
||||
// This version excludes PTR and DKIM checks since they require email context
|
||||
func (d *DNSAnalyzer) CalculateDomainOnlyScore(results *api.DNSResults) (int, string) {
|
||||
if results == nil {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
// MX Records: 30 points (only one domain to check)
|
||||
mxScore := d.calculateMXScore(results)
|
||||
// Since calculateMXScore checks both From and RP domains,
|
||||
// and we only have From domain, we use the full score
|
||||
score += 30 * mxScore / 100
|
||||
|
||||
// SPF Records: 30 points
|
||||
score += 30 * d.calculateSPFScore(results) / 100
|
||||
|
||||
// DMARC Record: 40 points
|
||||
score += 40 * d.calculateDMARCScore(results) / 100
|
||||
|
||||
// BIMI Record: only bonus
|
||||
if results.BimiRecord != nil && results.BimiRecord.Valid {
|
||||
if score >= 100 {
|
||||
return 100, "A+"
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure score doesn't exceed maximum
|
||||
if score > 100 {
|
||||
score = 100
|
||||
}
|
||||
|
||||
// Ensure score is non-negative
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score, ScoreToGradeKind(score)
|
||||
}
|
||||
|
||||
// CalculateDNSScore calculates the DNS score from records results
|
||||
// Returns a score from 0-100 where higher is better
|
||||
// senderIP is the original sender IP address used for FCrDNS verification
|
||||
|
|
|
|||
|
|
@ -45,6 +45,26 @@ func ScoreToGrade(score int) string {
|
|||
}
|
||||
}
|
||||
|
||||
// ScoreToGradeKind converts a percentage score (0-100) to a letter grade, be kind in gradation
|
||||
func ScoreToGradeKind(score int) string {
|
||||
switch {
|
||||
case score > 100:
|
||||
return "A+"
|
||||
case score >= 90:
|
||||
return "A"
|
||||
case score >= 80:
|
||||
return "B"
|
||||
case score >= 60:
|
||||
return "C"
|
||||
case score >= 45:
|
||||
return "D"
|
||||
case score >= 30:
|
||||
return "E"
|
||||
default:
|
||||
return "F"
|
||||
}
|
||||
}
|
||||
|
||||
// ScoreToReportGrade converts a percentage score to an api.ReportGrade
|
||||
func ScoreToReportGrade(score int) api.ReportGrade {
|
||||
return api.ReportGrade(ScoreToGrade(score))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
176
web/src/routes/domain/+page.svelte
Normal file
176
web/src/routes/domain/+page.svelte
Normal 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>
|
||||
181
web/src/routes/domain/[domain]/+page.svelte
Normal file
181
web/src/routes/domain/[domain]/+page.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue