Domain alignment checks for DKIM
This commit is contained in:
parent
465da6d16a
commit
5b179e7b93
5 changed files with 408 additions and 81 deletions
|
|
@ -664,6 +664,21 @@ components:
|
|||
description: Reverse DNS (PTR record) for the IP address
|
||||
example: "mail.example.com"
|
||||
|
||||
DKIMDomainInfo:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
- org_domain
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
description: DKIM signature domain
|
||||
example: "mail.example.com"
|
||||
org_domain:
|
||||
type: string
|
||||
description: Organizational domain extracted from DKIM domain (using Public Suffix List)
|
||||
example: "example.com"
|
||||
|
||||
DomainAlignment:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -686,9 +701,8 @@ components:
|
|||
dkim_domains:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Domains from DKIM signatures
|
||||
example: ["example.com"]
|
||||
$ref: '#/components/schemas/DKIMDomainInfo'
|
||||
description: Domains from DKIM signatures with their organizational domains
|
||||
aligned:
|
||||
type: boolean
|
||||
description: Whether all domains align (strict alignment - exact match)
|
||||
|
|
|
|||
|
|
@ -52,13 +52,14 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int
|
|||
maxGrade := 6
|
||||
headers := *analysis.Headers
|
||||
|
||||
// RP and From alignment (20 points)
|
||||
if analysis.DomainAlignment.Aligned != nil && *analysis.DomainAlignment.Aligned {
|
||||
score += 20
|
||||
} else if analysis.DomainAlignment.RelaxedAligned != nil && *analysis.DomainAlignment.RelaxedAligned {
|
||||
score += 15
|
||||
} else {
|
||||
// RP and From alignment (25 points)
|
||||
if analysis.DomainAlignment.Aligned == nil || !*analysis.DomainAlignment.RelaxedAligned {
|
||||
// Bad domain alignment, cap grade to C
|
||||
maxGrade -= 2
|
||||
} else if *analysis.DomainAlignment.Aligned {
|
||||
score += 25
|
||||
} else if *analysis.DomainAlignment.RelaxedAligned {
|
||||
score += 20
|
||||
}
|
||||
|
||||
// Check required headers (RFC 5322) - 30 points
|
||||
|
|
@ -79,7 +80,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int
|
|||
maxGrade = 1
|
||||
}
|
||||
|
||||
// Check recommended headers (20 points)
|
||||
// Check recommended headers (15 points)
|
||||
recommendedHeaders := []string{"subject", "to"}
|
||||
|
||||
// Add reply-to when from is a no-reply address
|
||||
|
|
@ -95,7 +96,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int
|
|||
presentRecommended++
|
||||
}
|
||||
}
|
||||
score += presentRecommended * 20 / recommendedCount
|
||||
score += presentRecommended * 15 / recommendedCount
|
||||
|
||||
if presentRecommended < recommendedCount {
|
||||
maxGrade -= 1
|
||||
|
|
@ -235,7 +236,7 @@ func (h *HeaderAnalyzer) formatAddress(addr *mail.Address) string {
|
|||
}
|
||||
|
||||
// GenerateHeaderAnalysis creates structured header analysis from email
|
||||
func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage) *api.HeaderAnalysis {
|
||||
func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults *api.AuthenticationResults) *api.HeaderAnalysis {
|
||||
if email == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -281,7 +282,7 @@ func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage) *api.Header
|
|||
}
|
||||
|
||||
// Domain alignment
|
||||
domainAlignment := h.analyzeDomainAlignment(email)
|
||||
domainAlignment := h.analyzeDomainAlignment(email, authResults)
|
||||
if domainAlignment != nil {
|
||||
analysis.DomainAlignment = domainAlignment
|
||||
}
|
||||
|
|
@ -352,8 +353,8 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp
|
|||
return check
|
||||
}
|
||||
|
||||
// analyzeDomainAlignment checks domain alignment between headers
|
||||
func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.DomainAlignment {
|
||||
// analyzeDomainAlignment checks domain alignment between headers and DKIM signatures
|
||||
func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults *api.AuthenticationResults) *api.DomainAlignment {
|
||||
alignment := &api.DomainAlignment{
|
||||
Aligned: api.PtrTo(true),
|
||||
RelaxedAligned: api.PtrTo(true),
|
||||
|
|
@ -383,14 +384,45 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.Domain
|
|||
}
|
||||
}
|
||||
|
||||
// Extract DKIM domains from authentication results
|
||||
var dkimDomains []api.DKIMDomainInfo
|
||||
if authResults != nil && authResults.Dkim != nil {
|
||||
for _, dkim := range *authResults.Dkim {
|
||||
if dkim.Domain != nil && *dkim.Domain != "" {
|
||||
domain := *dkim.Domain
|
||||
orgDomain := h.getOrganizationalDomain(domain)
|
||||
dkimDomains = append(dkimDomains, api.DKIMDomainInfo{
|
||||
Domain: domain,
|
||||
OrgDomain: orgDomain,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dkimDomains) > 0 {
|
||||
alignment.DkimDomains = &dkimDomains
|
||||
}
|
||||
|
||||
// Check alignment (strict and relaxed)
|
||||
issues := []string{}
|
||||
if alignment.FromDomain != nil && alignment.ReturnPathDomain != nil {
|
||||
|
||||
// hasReturnPath and hasDKIM track whether we have these fields to check
|
||||
hasReturnPath := alignment.FromDomain != nil && alignment.ReturnPathDomain != nil
|
||||
hasDKIM := alignment.FromDomain != nil && len(dkimDomains) > 0
|
||||
|
||||
// If neither Return-Path nor DKIM is present, keep default alignment (true)
|
||||
// Otherwise, at least one must be aligned for overall alignment to be true
|
||||
strictAligned := !hasReturnPath && !hasDKIM
|
||||
relaxedAligned := !hasReturnPath && !hasDKIM
|
||||
|
||||
// Check Return-Path alignment
|
||||
rpStrictAligned := false
|
||||
rpRelaxedAligned := false
|
||||
if hasReturnPath {
|
||||
fromDomain := *alignment.FromDomain
|
||||
rpDomain := *alignment.ReturnPathDomain
|
||||
|
||||
// Strict alignment: exact match (case-insensitive)
|
||||
strictAligned := strings.EqualFold(fromDomain, rpDomain)
|
||||
rpStrictAligned = strings.EqualFold(fromDomain, rpDomain)
|
||||
|
||||
// Relaxed alignment: organizational domain match
|
||||
var fromOrgDomain, rpOrgDomain string
|
||||
|
|
@ -400,20 +432,67 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.Domain
|
|||
if alignment.ReturnPathOrgDomain != nil {
|
||||
rpOrgDomain = *alignment.ReturnPathOrgDomain
|
||||
}
|
||||
relaxedAligned := strings.EqualFold(fromOrgDomain, rpOrgDomain)
|
||||
rpRelaxedAligned = strings.EqualFold(fromOrgDomain, rpOrgDomain)
|
||||
|
||||
*alignment.Aligned = strictAligned
|
||||
*alignment.RelaxedAligned = relaxedAligned
|
||||
|
||||
if !strictAligned {
|
||||
if relaxedAligned {
|
||||
if !rpStrictAligned {
|
||||
if rpRelaxedAligned {
|
||||
issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not exactly match From domain (%s), but satisfies relaxed alignment (organizational domain: %s)", rpDomain, fromDomain, fromOrgDomain))
|
||||
} else {
|
||||
issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not match From domain (%s) - neither strict nor relaxed alignment", rpDomain, fromDomain))
|
||||
}
|
||||
}
|
||||
|
||||
strictAligned = rpStrictAligned
|
||||
relaxedAligned = rpRelaxedAligned
|
||||
}
|
||||
|
||||
// Check DKIM alignment
|
||||
dkimStrictAligned := false
|
||||
dkimRelaxedAligned := false
|
||||
if hasDKIM {
|
||||
fromDomain := *alignment.FromDomain
|
||||
var fromOrgDomain string
|
||||
if alignment.FromOrgDomain != nil {
|
||||
fromOrgDomain = *alignment.FromOrgDomain
|
||||
}
|
||||
|
||||
for _, dkimDomain := range dkimDomains {
|
||||
// Check strict alignment for this DKIM signature
|
||||
if strings.EqualFold(fromDomain, dkimDomain.Domain) {
|
||||
dkimStrictAligned = true
|
||||
}
|
||||
|
||||
// Check relaxed alignment for this DKIM signature
|
||||
if strings.EqualFold(fromOrgDomain, dkimDomain.OrgDomain) {
|
||||
dkimRelaxedAligned = true
|
||||
}
|
||||
}
|
||||
|
||||
if !dkimStrictAligned && !dkimRelaxedAligned {
|
||||
// List all DKIM domains that failed alignment
|
||||
dkimDomainsList := []string{}
|
||||
for _, dkimDomain := range dkimDomains {
|
||||
dkimDomainsList = append(dkimDomainsList, dkimDomain.Domain)
|
||||
}
|
||||
issues = append(issues, fmt.Sprintf("DKIM signature domains (%s) do not align with From domain (%s) - neither strict nor relaxed alignment", strings.Join(dkimDomainsList, ", "), fromDomain))
|
||||
} else if !dkimStrictAligned && dkimRelaxedAligned {
|
||||
// DKIM has relaxed alignment but not strict
|
||||
issues = append(issues, fmt.Sprintf("DKIM signature domains satisfy relaxed alignment with From domain (%s) but not strict alignment (organizational domain: %s)", fromDomain, fromOrgDomain))
|
||||
}
|
||||
|
||||
// Overall alignment requires at least one method (Return-Path OR DKIM) to be aligned
|
||||
// For DMARC compliance, at least one of SPF or DKIM must be aligned
|
||||
if dkimStrictAligned {
|
||||
strictAligned = true
|
||||
}
|
||||
if dkimRelaxedAligned {
|
||||
relaxedAligned = true
|
||||
}
|
||||
}
|
||||
|
||||
*alignment.Aligned = strictAligned
|
||||
*alignment.RelaxedAligned = relaxedAligned
|
||||
|
||||
if len(issues) > 0 {
|
||||
alignment.Issues = &issues
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ package analyzer
|
|||
import (
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.happydns.org/happyDeliver/internal/api"
|
||||
|
|
@ -110,7 +111,7 @@ func TestCalculateHeaderScore(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Generate header analysis first
|
||||
analysis := analyzer.GenerateHeaderAnalysis(tt.email)
|
||||
analysis := analyzer.GenerateHeaderAnalysis(tt.email, nil)
|
||||
score, _ := analyzer.CalculateHeaderScore(analysis)
|
||||
if score < tt.minScore || score > tt.maxScore {
|
||||
t.Errorf("CalculateHeaderScore() = %v, want between %v and %v", score, tt.minScore, tt.maxScore)
|
||||
|
|
@ -360,7 +361,7 @@ func TestAnalyzeDomainAlignment(t *testing.T) {
|
|||
}),
|
||||
}
|
||||
|
||||
alignment := analyzer.analyzeDomainAlignment(email)
|
||||
alignment := analyzer.analyzeDomainAlignment(email, nil)
|
||||
|
||||
if alignment == nil {
|
||||
t.Fatal("Expected non-nil alignment")
|
||||
|
|
@ -698,7 +699,7 @@ func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) {
|
|||
"from relay.example.com (relay.example.com [192.0.2.2]) by mail.example.com with SMTP id DEF456; Mon, 01 Jan 2024 11:59:00 +0000",
|
||||
}
|
||||
|
||||
analysis := analyzer.GenerateHeaderAnalysis(email)
|
||||
analysis := analyzer.GenerateHeaderAnalysis(email, nil)
|
||||
|
||||
if analysis == nil {
|
||||
t.Fatal("GenerateHeaderAnalysis returned nil")
|
||||
|
|
@ -923,3 +924,156 @@ func equalStrPtr(a, b *string) bool {
|
|||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func TestAnalyzeDomainAlignment_WithDKIM(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fromHeader string
|
||||
returnPath string
|
||||
dkimDomains []string
|
||||
expectStrictAligned bool
|
||||
expectRelaxedAligned bool
|
||||
expectIssuesContain string
|
||||
}{
|
||||
{
|
||||
name: "DKIM strict alignment with From domain",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "",
|
||||
dkimDomains: []string{"example.com"},
|
||||
expectStrictAligned: true,
|
||||
expectRelaxedAligned: true,
|
||||
expectIssuesContain: "",
|
||||
},
|
||||
{
|
||||
name: "DKIM relaxed alignment only",
|
||||
fromHeader: "sender@mail.example.com",
|
||||
returnPath: "",
|
||||
dkimDomains: []string{"example.com"},
|
||||
expectStrictAligned: false,
|
||||
expectRelaxedAligned: true,
|
||||
expectIssuesContain: "relaxed alignment",
|
||||
},
|
||||
{
|
||||
name: "DKIM no alignment",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "",
|
||||
dkimDomains: []string{"different.com"},
|
||||
expectStrictAligned: false,
|
||||
expectRelaxedAligned: false,
|
||||
expectIssuesContain: "do not align",
|
||||
},
|
||||
{
|
||||
name: "Multiple DKIM signatures - one aligns",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "",
|
||||
dkimDomains: []string{"different.com", "example.com"},
|
||||
expectStrictAligned: true,
|
||||
expectRelaxedAligned: true,
|
||||
expectIssuesContain: "",
|
||||
},
|
||||
{
|
||||
name: "Return-Path misaligned but DKIM aligned",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "bounce@different.com",
|
||||
dkimDomains: []string{"example.com"},
|
||||
expectStrictAligned: true,
|
||||
expectRelaxedAligned: true,
|
||||
expectIssuesContain: "Return-Path",
|
||||
},
|
||||
{
|
||||
name: "Return-Path aligned, no DKIM",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "bounce@example.com",
|
||||
dkimDomains: []string{},
|
||||
expectStrictAligned: true,
|
||||
expectRelaxedAligned: true,
|
||||
expectIssuesContain: "",
|
||||
},
|
||||
{
|
||||
name: "Both Return-Path and DKIM misaligned",
|
||||
fromHeader: "sender@example.com",
|
||||
returnPath: "bounce@other.com",
|
||||
dkimDomains: []string{"different.com"},
|
||||
expectStrictAligned: false,
|
||||
expectRelaxedAligned: false,
|
||||
expectIssuesContain: "do not",
|
||||
},
|
||||
}
|
||||
|
||||
analyzer := NewHeaderAnalyzer()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
email := &EmailMessage{
|
||||
Header: createHeaderWithFields(map[string]string{
|
||||
"From": tt.fromHeader,
|
||||
"Return-Path": tt.returnPath,
|
||||
}),
|
||||
}
|
||||
|
||||
// Create authentication results with DKIM signatures
|
||||
var authResults *api.AuthenticationResults
|
||||
if len(tt.dkimDomains) > 0 {
|
||||
dkimResults := make([]api.AuthResult, 0, len(tt.dkimDomains))
|
||||
for _, domain := range tt.dkimDomains {
|
||||
dkimResults = append(dkimResults, api.AuthResult{
|
||||
Result: api.AuthResultResultPass,
|
||||
Domain: &domain,
|
||||
})
|
||||
}
|
||||
authResults = &api.AuthenticationResults{
|
||||
Dkim: &dkimResults,
|
||||
}
|
||||
}
|
||||
|
||||
alignment := analyzer.analyzeDomainAlignment(email, authResults)
|
||||
|
||||
if alignment == nil {
|
||||
t.Fatal("Expected non-nil alignment")
|
||||
}
|
||||
|
||||
if alignment.Aligned == nil {
|
||||
t.Fatal("Expected non-nil Aligned field")
|
||||
}
|
||||
|
||||
if *alignment.Aligned != tt.expectStrictAligned {
|
||||
t.Errorf("Aligned = %v, want %v", *alignment.Aligned, tt.expectStrictAligned)
|
||||
}
|
||||
|
||||
if alignment.RelaxedAligned == nil {
|
||||
t.Fatal("Expected non-nil RelaxedAligned field")
|
||||
}
|
||||
|
||||
if *alignment.RelaxedAligned != tt.expectRelaxedAligned {
|
||||
t.Errorf("RelaxedAligned = %v, want %v", *alignment.RelaxedAligned, tt.expectRelaxedAligned)
|
||||
}
|
||||
|
||||
// Check DKIM domains are populated
|
||||
if len(tt.dkimDomains) > 0 {
|
||||
if alignment.DkimDomains == nil {
|
||||
t.Error("Expected DkimDomains to be populated")
|
||||
} else if len(*alignment.DkimDomains) != len(tt.dkimDomains) {
|
||||
t.Errorf("Expected %d DKIM domains, got %d", len(tt.dkimDomains), len(*alignment.DkimDomains))
|
||||
}
|
||||
}
|
||||
|
||||
// Check issues contain expected string
|
||||
if tt.expectIssuesContain != "" {
|
||||
if alignment.Issues == nil || len(*alignment.Issues) == 0 {
|
||||
t.Errorf("Expected issues to contain '%s', but no issues found", tt.expectIssuesContain)
|
||||
} else {
|
||||
found := false
|
||||
for _, issue := range *alignment.Issues {
|
||||
if strings.Contains(strings.ToLower(issue), strings.ToLower(tt.expectIssuesContain)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected issues to contain '%s', but found: %v", tt.expectIssuesContain, *alignment.Issues)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
|
|||
|
||||
// Run all analyzers
|
||||
results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email)
|
||||
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email)
|
||||
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
|
||||
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers)
|
||||
results.RBL = r.rblChecker.CheckEmail(email)
|
||||
results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email)
|
||||
|
|
|
|||
|
|
@ -66,68 +66,148 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text small text-muted mb-3">
|
||||
Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path). Proper alignment is crucial for DMARC compliance and helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers.
|
||||
<p class="card-text small text-muted">
|
||||
Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path or DKIM signature). Proper alignment is crucial for DMARC compliance, regardless of the policy. It helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. Only one of the following lines needs to pass.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Strict Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={headerAnalysis.domain_alignment.aligned} class:bg-danger={!headerAnalysis.domain_alignment.aligned}>
|
||||
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{headerAnalysis.domain_alignment.aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Exact domain match</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item d-flex ps-0">
|
||||
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
|
||||
SPF
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Relaxed Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={headerAnalysis.domain_alignment.relaxed_aligned} class:bg-danger={!headerAnalysis.domain_alignment.relaxed_aligned}>
|
||||
<i class="bi {headerAnalysis.domain_alignment.relaxed_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{headerAnalysis.domain_alignment.relaxed_aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Organizational domain match</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">From Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Return-Path Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
|
||||
{#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.return_path_org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
||||
<div class="mt-3">
|
||||
{#each headerAnalysis.domain_alignment.issues as issue}
|
||||
<div class="alert alert-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info' : 'warning'} py-2 small mb-2">
|
||||
<i class="bi bi-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info-circle' : 'exclamation-triangle'} me-1"></i>
|
||||
{issue}
|
||||
<div class="row flex-grow-1">
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Strict Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={headerAnalysis.domain_alignment.aligned} class:bg-danger={!headerAnalysis.domain_alignment.aligned}>
|
||||
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{headerAnalysis.domain_alignment.aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="small text-muted mt-1">Exact domain match</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Relaxed Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={headerAnalysis.domain_alignment.relaxed_aligned} class:bg-danger={!headerAnalysis.domain_alignment.relaxed_aligned}>
|
||||
<i class="bi {headerAnalysis.domain_alignment.relaxed_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{headerAnalysis.domain_alignment.relaxed_aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Organizational domain match</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">From Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Return-Path Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
|
||||
{#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.return_path_org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
||||
<div class="mt-3">
|
||||
{#each headerAnalysis.domain_alignment.issues as issue}
|
||||
<div class="alert alert-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info' : 'warning'} py-2 small mb-2">
|
||||
<i class="bi bi-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info-circle' : 'exclamation-triangle'} me-1"></i>
|
||||
{issue}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Alignment Information based on DMARC policy -->
|
||||
{#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
|
||||
{#if dmarcRecord.spf_alignment === 'strict'}
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
<strong>Strict SPF alignment required</strong> — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment.
|
||||
{:else}
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass.
|
||||
{/if}
|
||||
<!-- Alignment Information based on DMARC policy -->
|
||||
{#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
|
||||
{#if dmarcRecord.spf_alignment === 'strict'}
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
<strong>Strict SPF alignment required</strong> — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment.
|
||||
{:else}
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass.
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain}
|
||||
{@const dkim_aligned = dkim_domain.domain === headerAnalysis.domain_alignment.from_domain}
|
||||
{@const dkim_relaxed_aligned = dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
|
||||
<div class="list-group-item d-flex ps-0">
|
||||
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
|
||||
DKIM
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<div class="row flex-grow-1">
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Strict Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={dkim_aligned} class:bg-danger={!dkim_aligned}>
|
||||
<i class="bi {dkim_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{dkim_aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Exact domain match</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Relaxed Alignment</small>
|
||||
<div>
|
||||
<span class="badge" class:bg-success={dkim_relaxed_aligned} class:bg-danger={!dkim_relaxed_aligned}>
|
||||
<i class="bi {dkim_relaxed_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||
<strong>{dkim_relaxed_aligned ? 'Pass' : 'Fail'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Organizational domain match</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">From Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Signature Domain</small>
|
||||
<div><code>{dkim_domain.domain || '-'}</code></div>
|
||||
{#if dkim_domain.domain !== dkim_domain.org_domain}
|
||||
<div class="small text-muted mt-1">Org: <code>{dkim_domain.org_domain}</code></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
||||
<div class="mt-3">
|
||||
{#each headerAnalysis.domain_alignment.issues as issue}
|
||||
<div class="alert alert-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info' : 'warning'} py-2 small mb-2">
|
||||
<i class="bi bi-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info-circle' : 'exclamation-triangle'} me-1"></i>
|
||||
{issue}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Alignment Information based on DMARC policy -->
|
||||
{#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain}
|
||||
{#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
|
||||
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.dkim_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
|
||||
{#if dmarcRecord.dkim_alignment === 'strict'}
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
<strong>Strict DKIM alignment required</strong> — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment.
|
||||
{:else}
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Relaxed DKIM alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass.
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue