Compare commits
1 commit
dcf357e4e2
...
06aacdbe2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 06aacdbe2b |
15 changed files with 115 additions and 265 deletions
|
|
@ -350,19 +350,6 @@ components:
|
||||||
listed: false
|
listed: false
|
||||||
- rbl: "bl.spamcop.net"
|
- rbl: "bl.spamcop.net"
|
||||||
listed: false
|
listed: false
|
||||||
whitelists:
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/BlacklistCheck'
|
|
||||||
description: Map of IP addresses to their DNS whitelist check results (informational only)
|
|
||||||
example:
|
|
||||||
"192.0.2.1":
|
|
||||||
- rbl: "list.dnswl.org"
|
|
||||||
listed: false
|
|
||||||
- rbl: "swl.spamhaus.org"
|
|
||||||
listed: false
|
|
||||||
content_analysis:
|
content_analysis:
|
||||||
$ref: '#/components/schemas/ContentAnalysis'
|
$ref: '#/components/schemas/ContentAnalysis'
|
||||||
header_analysis:
|
header_analysis:
|
||||||
|
|
@ -789,7 +776,7 @@ components:
|
||||||
properties:
|
properties:
|
||||||
result:
|
result:
|
||||||
type: string
|
type: string
|
||||||
enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass, skipped]
|
enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass]
|
||||||
description: Authentication result
|
description: Authentication result
|
||||||
example: "pass"
|
example: "pass"
|
||||||
domain:
|
domain:
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ type AnalysisConfig struct {
|
||||||
DNSTimeout time.Duration
|
DNSTimeout time.Duration
|
||||||
HTTPTimeout time.Duration
|
HTTPTimeout time.Duration
|
||||||
RBLs []string
|
RBLs []string
|
||||||
DNSWLs []string
|
|
||||||
CheckAllIPs bool // Check all IPs found in headers, not just the first one
|
CheckAllIPs bool // Check all IPs found in headers, not just the first one
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +88,6 @@ func DefaultConfig() *Config {
|
||||||
DNSTimeout: 5 * time.Second,
|
DNSTimeout: 5 * time.Second,
|
||||||
HTTPTimeout: 10 * time.Second,
|
HTTPTimeout: 10 * time.Second,
|
||||||
RBLs: []string{},
|
RBLs: []string{},
|
||||||
DNSWLs: []string{},
|
|
||||||
CheckAllIPs: false, // By default, only check the first IP
|
CheckAllIPs: false, // By default, only check the first IP
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ func NewEmailAnalyzer(cfg *config.Config) *EmailAnalyzer {
|
||||||
cfg.Analysis.DNSTimeout,
|
cfg.Analysis.DNSTimeout,
|
||||||
cfg.Analysis.HTTPTimeout,
|
cfg.Analysis.HTTPTimeout,
|
||||||
cfg.Analysis.RBLs,
|
cfg.Analysis.RBLs,
|
||||||
cfg.Analysis.DNSWLs,
|
|
||||||
cfg.Analysis.CheckAllIPs,
|
cfg.Analysis.CheckAllIPs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -131,12 +130,12 @@ func (a *APIAdapter) CheckBlacklistIP(ip string) ([]api.BlacklistCheck, int, int
|
||||||
|
|
||||||
// Calculate score using the existing function
|
// Calculate score using the existing function
|
||||||
// Create a minimal RBLResults structure for scoring
|
// Create a minimal RBLResults structure for scoring
|
||||||
results := &DNSListResults{
|
results := &RBLResults{
|
||||||
Checks: map[string][]api.BlacklistCheck{ip: checks},
|
Checks: map[string][]api.BlacklistCheck{ip: checks},
|
||||||
IPsChecked: []string{ip},
|
IPsChecked: []string{ip},
|
||||||
ListedCount: listedCount,
|
ListedCount: listedCount,
|
||||||
}
|
}
|
||||||
score, grade := a.analyzer.generator.rblChecker.CalculateScore(results)
|
score, grade := a.analyzer.generator.rblChecker.CalculateRBLScore(results)
|
||||||
|
|
||||||
return checks, listedCount, score, grade, nil
|
return checks, listedCount, score, grade, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,12 @@ import (
|
||||||
"git.happydns.org/happyDeliver/internal/api"
|
"git.happydns.org/happyDeliver/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSListChecker checks IP addresses against DNS-based block/allow lists.
|
// RBLChecker checks IP addresses against DNS-based blacklists
|
||||||
// It handles both RBL (blacklist) and DNSWL (whitelist) semantics via flags.
|
type RBLChecker struct {
|
||||||
type DNSListChecker struct {
|
Timeout time.Duration
|
||||||
Timeout time.Duration
|
RBLs []string
|
||||||
Lists []string
|
CheckAllIPs bool // Check all IPs found in headers, not just the first one
|
||||||
CheckAllIPs bool // Check all IPs found in headers, not just the first one
|
resolver *net.Resolver
|
||||||
filterErrorCodes bool // When true (RBL mode), treat 127.255.255.253/254/255 as operational errors
|
|
||||||
resolver *net.Resolver
|
|
||||||
informationalSet map[string]bool // Lists whose hits don't count toward the score
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRBLs is a list of commonly used RBL providers
|
// DefaultRBLs is a list of commonly used RBL providers
|
||||||
|
|
@ -51,85 +48,40 @@ var DefaultRBLs = []string{
|
||||||
"b.barracudacentral.org", // Barracuda
|
"b.barracudacentral.org", // Barracuda
|
||||||
"cbl.abuseat.org", // CBL (Composite Blocking List)
|
"cbl.abuseat.org", // CBL (Composite Blocking List)
|
||||||
"dnsbl-1.uceprotect.net", // UCEPROTECT Level 1
|
"dnsbl-1.uceprotect.net", // UCEPROTECT Level 1
|
||||||
"dnsbl-2.uceprotect.net", // UCEPROTECT Level 2 (informational)
|
|
||||||
"dnsbl-3.uceprotect.net", // UCEPROTECT Level 3 (informational)
|
|
||||||
"spam.spamrats.com", // SpamRats SPAM
|
|
||||||
"dyna.spamrats.com", // SpamRats dynamic IPs
|
|
||||||
"psbl.surriel.com", // PSBL
|
|
||||||
"dnsbl.dronebl.org", // DroneBL
|
|
||||||
"bl.mailspike.net", // Mailspike BL
|
|
||||||
"z.mailspike.net", // Mailspike Z
|
|
||||||
"bl.rbl-dns.com", // RBL-DNS
|
|
||||||
"bl.nszones.com", // NSZones
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultInformationalRBLs lists RBLs that are checked but not counted in the score.
|
|
||||||
// These are typically broader lists where being listed is less definitive.
|
|
||||||
var DefaultInformationalRBLs = []string{
|
|
||||||
"dnsbl-2.uceprotect.net", // UCEPROTECT Level 2: entire netblocks, may cause false positives
|
|
||||||
"dnsbl-3.uceprotect.net", // UCEPROTECT Level 3: entire ASes, too broad for scoring
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultDNSWLs is a list of commonly used DNSWL providers
|
|
||||||
var DefaultDNSWLs = []string{
|
|
||||||
"list.dnswl.org", // DNSWL.org — the main DNS whitelist
|
|
||||||
"swl.spamhaus.org", // Spamhaus Safe Whitelist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRBLChecker creates a new RBL checker with configurable timeout and RBL list
|
// NewRBLChecker creates a new RBL checker with configurable timeout and RBL list
|
||||||
func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *DNSListChecker {
|
func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLChecker {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = 5 * time.Second
|
timeout = 5 * time.Second // Default timeout
|
||||||
}
|
}
|
||||||
if len(rbls) == 0 {
|
if len(rbls) == 0 {
|
||||||
rbls = DefaultRBLs
|
rbls = DefaultRBLs
|
||||||
}
|
}
|
||||||
informationalSet := make(map[string]bool, len(DefaultInformationalRBLs))
|
return &RBLChecker{
|
||||||
for _, rbl := range DefaultInformationalRBLs {
|
Timeout: timeout,
|
||||||
informationalSet[rbl] = true
|
RBLs: rbls,
|
||||||
}
|
CheckAllIPs: checkAllIPs,
|
||||||
return &DNSListChecker{
|
resolver: &net.Resolver{
|
||||||
Timeout: timeout,
|
PreferGo: true,
|
||||||
Lists: rbls,
|
},
|
||||||
CheckAllIPs: checkAllIPs,
|
|
||||||
filterErrorCodes: true,
|
|
||||||
resolver: &net.Resolver{PreferGo: true},
|
|
||||||
informationalSet: informationalSet,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSWLChecker creates a new DNSWL checker with configurable timeout and DNSWL list
|
// RBLResults represents the results of RBL checks
|
||||||
func NewDNSWLChecker(timeout time.Duration, dnswls []string, checkAllIPs bool) *DNSListChecker {
|
type RBLResults struct {
|
||||||
if timeout == 0 {
|
Checks map[string][]api.BlacklistCheck // Map of IP -> list of RBL checks for that IP
|
||||||
timeout = 5 * time.Second
|
IPsChecked []string
|
||||||
}
|
ListedCount int
|
||||||
if len(dnswls) == 0 {
|
|
||||||
dnswls = DefaultDNSWLs
|
|
||||||
}
|
|
||||||
return &DNSListChecker{
|
|
||||||
Timeout: timeout,
|
|
||||||
Lists: dnswls,
|
|
||||||
CheckAllIPs: checkAllIPs,
|
|
||||||
filterErrorCodes: false,
|
|
||||||
resolver: &net.Resolver{PreferGo: true},
|
|
||||||
informationalSet: make(map[string]bool),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSListResults represents the results of DNS list checks
|
// CheckEmail checks all IPs found in the email headers against RBLs
|
||||||
type DNSListResults struct {
|
func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults {
|
||||||
Checks map[string][]api.BlacklistCheck // Map of IP -> list of checks for that IP
|
results := &RBLResults{
|
||||||
IPsChecked []string
|
|
||||||
ListedCount int // Total listings including informational entries
|
|
||||||
RelevantListedCount int // Listings on scoring (non-informational) lists only
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckEmail checks all IPs found in the email headers against the configured lists
|
|
||||||
func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults {
|
|
||||||
results := &DNSListResults{
|
|
||||||
Checks: make(map[string][]api.BlacklistCheck),
|
Checks: make(map[string][]api.BlacklistCheck),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract IPs from Received headers
|
||||||
ips := r.extractIPs(email)
|
ips := r.extractIPs(email)
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
return results
|
return results
|
||||||
|
|
@ -137,18 +89,17 @@ func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults {
|
||||||
|
|
||||||
results.IPsChecked = ips
|
results.IPsChecked = ips
|
||||||
|
|
||||||
|
// Check each IP against all RBLs
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
for _, list := range r.Lists {
|
for _, rbl := range r.RBLs {
|
||||||
check := r.checkIP(ip, list)
|
check := r.checkIP(ip, rbl)
|
||||||
results.Checks[ip] = append(results.Checks[ip], check)
|
results.Checks[ip] = append(results.Checks[ip], check)
|
||||||
if check.Listed {
|
if check.Listed {
|
||||||
results.ListedCount++
|
results.ListedCount++
|
||||||
if !r.informationalSet[list] {
|
|
||||||
results.RelevantListedCount++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only check the first IP unless CheckAllIPs is enabled
|
||||||
if !r.CheckAllIPs {
|
if !r.CheckAllIPs {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -157,8 +108,9 @@ func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckIP checks a single IP address against all configured lists
|
// CheckIP checks a single IP address against all configured RBLs
|
||||||
func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) {
|
func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) {
|
||||||
|
// Validate that it's a valid IP address
|
||||||
if !r.isPublicIP(ip) {
|
if !r.isPublicIP(ip) {
|
||||||
return nil, 0, fmt.Errorf("invalid or non-public IP address: %s", ip)
|
return nil, 0, fmt.Errorf("invalid or non-public IP address: %s", ip)
|
||||||
}
|
}
|
||||||
|
|
@ -166,8 +118,9 @@ func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) {
|
||||||
var checks []api.BlacklistCheck
|
var checks []api.BlacklistCheck
|
||||||
listedCount := 0
|
listedCount := 0
|
||||||
|
|
||||||
for _, list := range r.Lists {
|
// Check the IP against all RBLs
|
||||||
check := r.checkIP(ip, list)
|
for _, rbl := range r.RBLs {
|
||||||
|
check := r.checkIP(ip, rbl)
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
if check.Listed {
|
if check.Listed {
|
||||||
listedCount++
|
listedCount++
|
||||||
|
|
@ -178,19 +131,27 @@ func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractIPs extracts IP addresses from Received headers
|
// extractIPs extracts IP addresses from Received headers
|
||||||
func (r *DNSListChecker) extractIPs(email *EmailMessage) []string {
|
func (r *RBLChecker) extractIPs(email *EmailMessage) []string {
|
||||||
var ips []string
|
var ips []string
|
||||||
seenIPs := make(map[string]bool)
|
seenIPs := make(map[string]bool)
|
||||||
|
|
||||||
|
// Get all Received headers
|
||||||
receivedHeaders := email.Header["Received"]
|
receivedHeaders := email.Header["Received"]
|
||||||
|
|
||||||
|
// Regex patterns for IP addresses
|
||||||
|
// Match IPv4: xxx.xxx.xxx.xxx
|
||||||
ipv4Pattern := regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`)
|
ipv4Pattern := regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`)
|
||||||
|
|
||||||
|
// Look for IPs in Received headers
|
||||||
for _, received := range receivedHeaders {
|
for _, received := range receivedHeaders {
|
||||||
|
// Find all IPv4 addresses
|
||||||
matches := ipv4Pattern.FindAllString(received, -1)
|
matches := ipv4Pattern.FindAllString(received, -1)
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
|
// Skip private/reserved IPs
|
||||||
if !r.isPublicIP(match) {
|
if !r.isPublicIP(match) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Avoid duplicates
|
||||||
if !seenIPs[match] {
|
if !seenIPs[match] {
|
||||||
ips = append(ips, match)
|
ips = append(ips, match)
|
||||||
seenIPs[match] = true
|
seenIPs[match] = true
|
||||||
|
|
@ -198,10 +159,13 @@ func (r *DNSListChecker) extractIPs(email *EmailMessage) []string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no IPs found in Received headers, try X-Originating-IP
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
originatingIP := email.Header.Get("X-Originating-IP")
|
originatingIP := email.Header.Get("X-Originating-IP")
|
||||||
if originatingIP != "" {
|
if originatingIP != "" {
|
||||||
|
// Extract IP from formats like "[192.0.2.1]" or "192.0.2.1"
|
||||||
cleanIP := strings.TrimSuffix(strings.TrimPrefix(originatingIP, "["), "]")
|
cleanIP := strings.TrimSuffix(strings.TrimPrefix(originatingIP, "["), "]")
|
||||||
|
// Remove any whitespace
|
||||||
cleanIP = strings.TrimSpace(cleanIP)
|
cleanIP = strings.TrimSpace(cleanIP)
|
||||||
matches := ipv4Pattern.FindString(cleanIP)
|
matches := ipv4Pattern.FindString(cleanIP)
|
||||||
if matches != "" && r.isPublicIP(matches) {
|
if matches != "" && r.isPublicIP(matches) {
|
||||||
|
|
@ -214,16 +178,19 @@ func (r *DNSListChecker) extractIPs(email *EmailMessage) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPublicIP checks if an IP address is public (not private, loopback, or reserved)
|
// isPublicIP checks if an IP address is public (not private, loopback, or reserved)
|
||||||
func (r *DNSListChecker) isPublicIP(ipStr string) bool {
|
func (r *RBLChecker) isPublicIP(ipStr string) bool {
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a private network
|
||||||
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional checks for reserved ranges
|
||||||
|
// 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), 203.0.113.0/24 (TEST-NET-3)
|
||||||
if ip.IsUnspecified() {
|
if ip.IsUnspecified() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -231,43 +198,51 @@ func (r *DNSListChecker) isPublicIP(ipStr string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkIP checks a single IP against a single DNS list
|
// checkIP checks a single IP against a single RBL
|
||||||
func (r *DNSListChecker) checkIP(ip, list string) api.BlacklistCheck {
|
func (r *RBLChecker) checkIP(ip, rbl string) api.BlacklistCheck {
|
||||||
check := api.BlacklistCheck{
|
check := api.BlacklistCheck{
|
||||||
Rbl: list,
|
Rbl: rbl,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reverse the IP for DNSBL query
|
||||||
reversedIP := r.reverseIP(ip)
|
reversedIP := r.reverseIP(ip)
|
||||||
if reversedIP == "" {
|
if reversedIP == "" {
|
||||||
check.Error = api.PtrTo("Failed to reverse IP address")
|
check.Error = api.PtrTo("Failed to reverse IP address")
|
||||||
return check
|
return check
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf("%s.%s", reversedIP, list)
|
// Construct DNSBL query: reversed-ip.rbl-domain
|
||||||
|
query := fmt.Sprintf("%s.%s", reversedIP, rbl)
|
||||||
|
|
||||||
|
// Perform DNS lookup with timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), r.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), r.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
addrs, err := r.resolver.LookupHost(ctx, query)
|
addrs, err := r.resolver.LookupHost(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Most likely not listed (NXDOMAIN)
|
||||||
if dnsErr, ok := err.(*net.DNSError); ok {
|
if dnsErr, ok := err.(*net.DNSError); ok {
|
||||||
if dnsErr.IsNotFound {
|
if dnsErr.IsNotFound {
|
||||||
check.Listed = false
|
check.Listed = false
|
||||||
return check
|
return check
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Other DNS errors
|
||||||
check.Error = api.PtrTo(fmt.Sprintf("DNS lookup failed: %v", err))
|
check.Error = api.PtrTo(fmt.Sprintf("DNS lookup failed: %v", err))
|
||||||
return check
|
return check
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we got a response, check the return code
|
||||||
if len(addrs) > 0 {
|
if len(addrs) > 0 {
|
||||||
check.Response = api.PtrTo(addrs[0])
|
check.Response = api.PtrTo(addrs[0]) // Return code (e.g., 127.0.0.2)
|
||||||
|
|
||||||
// In RBL mode, 127.255.255.253/254/255 indicate operational errors, not real listings.
|
// Check for RBL error codes: 127.255.255.253, 127.255.255.254, 127.255.255.255
|
||||||
if r.filterErrorCodes && (addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255") {
|
// These indicate RBL operational issues, not actual listings
|
||||||
|
if addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255" {
|
||||||
check.Listed = false
|
check.Listed = false
|
||||||
check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", list, addrs[0]))
|
check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", rbl, addrs[0]))
|
||||||
} else {
|
} else {
|
||||||
|
// Normal listing response
|
||||||
check.Listed = true
|
check.Listed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,47 +250,44 @@ func (r *DNSListChecker) checkIP(ip, list string) api.BlacklistCheck {
|
||||||
return check
|
return check
|
||||||
}
|
}
|
||||||
|
|
||||||
// reverseIP reverses an IPv4 address for DNSBL/DNSWL queries
|
// reverseIP reverses an IPv4 address for DNSBL queries
|
||||||
// Example: 192.0.2.1 -> 1.2.0.192
|
// Example: 192.0.2.1 -> 1.2.0.192
|
||||||
func (r *DNSListChecker) reverseIP(ipStr string) string {
|
func (r *RBLChecker) reverseIP(ipStr string) string {
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert to IPv4
|
||||||
ipv4 := ip.To4()
|
ipv4 := ip.To4()
|
||||||
if ipv4 == nil {
|
if ipv4 == nil {
|
||||||
return "" // IPv6 not supported yet
|
return "" // IPv6 not supported yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reverse the octets
|
||||||
return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0])
|
return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateScore calculates the list contribution to deliverability.
|
// CalculateRBLScore calculates the blacklist contribution to deliverability
|
||||||
// Informational lists are not counted in the score.
|
func (r *RBLChecker) CalculateRBLScore(results *RBLResults) (int, string) {
|
||||||
func (r *DNSListChecker) CalculateScore(results *DNSListResults) (int, string) {
|
|
||||||
if results == nil || len(results.IPsChecked) == 0 {
|
if results == nil || len(results.IPsChecked) == 0 {
|
||||||
|
// No IPs to check, give benefit of doubt
|
||||||
return 100, ""
|
return 100, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
scoringListCount := len(r.Lists) - len(r.informationalSet)
|
percentage := 100 - results.ListedCount*100/len(r.RBLs)
|
||||||
if scoringListCount <= 0 {
|
|
||||||
return 100, "A+"
|
|
||||||
}
|
|
||||||
|
|
||||||
percentage := 100 - results.RelevantListedCount*100/scoringListCount
|
|
||||||
return percentage, ScoreToGrade(percentage)
|
return percentage, ScoreToGrade(percentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one entry
|
// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one RBL
|
||||||
func (r *DNSListChecker) GetUniqueListedIPs(results *DNSListResults) []string {
|
func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string {
|
||||||
var listedIPs []string
|
var listedIPs []string
|
||||||
|
|
||||||
for ip, checks := range results.Checks {
|
for ip, rblChecks := range results.Checks {
|
||||||
for _, check := range checks {
|
for _, check := range rblChecks {
|
||||||
if check.Listed {
|
if check.Listed {
|
||||||
listedIPs = append(listedIPs, ip)
|
listedIPs = append(listedIPs, ip)
|
||||||
break
|
break // Only add the IP once
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -323,17 +295,17 @@ func (r *DNSListChecker) GetUniqueListedIPs(results *DNSListResults) []string {
|
||||||
return listedIPs
|
return listedIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetListsForIP returns all lists that match a specific IP
|
// GetRBLsForIP returns all RBLs that list a specific IP
|
||||||
func (r *DNSListChecker) GetListsForIP(results *DNSListResults, ip string) []string {
|
func (r *RBLChecker) GetRBLsForIP(results *RBLResults, ip string) []string {
|
||||||
var lists []string
|
var rbls []string
|
||||||
|
|
||||||
if checks, exists := results.Checks[ip]; exists {
|
if rblChecks, exists := results.Checks[ip]; exists {
|
||||||
for _, check := range checks {
|
for _, check := range rblChecks {
|
||||||
if check.Listed {
|
if check.Listed {
|
||||||
lists = append(lists, check.Rbl)
|
rbls = append(rbls, check.Rbl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lists
|
return rbls
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ func TestNewRBLChecker(t *testing.T) {
|
||||||
if checker.Timeout != tt.expectedTimeout {
|
if checker.Timeout != tt.expectedTimeout {
|
||||||
t.Errorf("Timeout = %v, want %v", checker.Timeout, tt.expectedTimeout)
|
t.Errorf("Timeout = %v, want %v", checker.Timeout, tt.expectedTimeout)
|
||||||
}
|
}
|
||||||
if len(checker.Lists) != tt.expectedRBLs {
|
if len(checker.RBLs) != tt.expectedRBLs {
|
||||||
t.Errorf("RBLs count = %d, want %d", len(checker.Lists), tt.expectedRBLs)
|
t.Errorf("RBLs count = %d, want %d", len(checker.RBLs), tt.expectedRBLs)
|
||||||
}
|
}
|
||||||
if checker.resolver == nil {
|
if checker.resolver == nil {
|
||||||
t.Error("Resolver should not be nil")
|
t.Error("Resolver should not be nil")
|
||||||
|
|
@ -326,7 +326,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
score, _ := checker.CalculateScore(tt.results)
|
score, _ := checker.CalculateRBLScore(tt.results)
|
||||||
if score != tt.expectedScore {
|
if score != tt.expectedScore {
|
||||||
t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore)
|
t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore)
|
||||||
}
|
}
|
||||||
|
|
@ -402,7 +402,7 @@ func TestGetRBLsForIP(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
rbls := checker.GetListsForIP(results, tt.ip)
|
rbls := checker.GetRBLsForIP(results, tt.ip)
|
||||||
|
|
||||||
if len(rbls) != len(tt.expectedRBLs) {
|
if len(rbls) != len(tt.expectedRBLs) {
|
||||||
t.Errorf("Got %d RBLs, want %d", len(rbls), len(tt.expectedRBLs))
|
t.Errorf("Got %d RBLs, want %d", len(rbls), len(tt.expectedRBLs))
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,7 @@ type ReportGenerator struct {
|
||||||
spamAnalyzer *SpamAssassinAnalyzer
|
spamAnalyzer *SpamAssassinAnalyzer
|
||||||
rspamdAnalyzer *RspamdAnalyzer
|
rspamdAnalyzer *RspamdAnalyzer
|
||||||
dnsAnalyzer *DNSAnalyzer
|
dnsAnalyzer *DNSAnalyzer
|
||||||
rblChecker *DNSListChecker
|
rblChecker *RBLChecker
|
||||||
dnswlChecker *DNSListChecker
|
|
||||||
contentAnalyzer *ContentAnalyzer
|
contentAnalyzer *ContentAnalyzer
|
||||||
headerAnalyzer *HeaderAnalyzer
|
headerAnalyzer *HeaderAnalyzer
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +45,6 @@ func NewReportGenerator(
|
||||||
dnsTimeout time.Duration,
|
dnsTimeout time.Duration,
|
||||||
httpTimeout time.Duration,
|
httpTimeout time.Duration,
|
||||||
rbls []string,
|
rbls []string,
|
||||||
dnswls []string,
|
|
||||||
checkAllIPs bool,
|
checkAllIPs bool,
|
||||||
) *ReportGenerator {
|
) *ReportGenerator {
|
||||||
return &ReportGenerator{
|
return &ReportGenerator{
|
||||||
|
|
@ -55,7 +53,6 @@ func NewReportGenerator(
|
||||||
rspamdAnalyzer: NewRspamdAnalyzer(),
|
rspamdAnalyzer: NewRspamdAnalyzer(),
|
||||||
dnsAnalyzer: NewDNSAnalyzer(dnsTimeout),
|
dnsAnalyzer: NewDNSAnalyzer(dnsTimeout),
|
||||||
rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs),
|
rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs),
|
||||||
dnswlChecker: NewDNSWLChecker(dnsTimeout, dnswls, checkAllIPs),
|
|
||||||
contentAnalyzer: NewContentAnalyzer(httpTimeout),
|
contentAnalyzer: NewContentAnalyzer(httpTimeout),
|
||||||
headerAnalyzer: NewHeaderAnalyzer(),
|
headerAnalyzer: NewHeaderAnalyzer(),
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +65,7 @@ type AnalysisResults struct {
|
||||||
Content *ContentResults
|
Content *ContentResults
|
||||||
DNS *api.DNSResults
|
DNS *api.DNSResults
|
||||||
Headers *api.HeaderAnalysis
|
Headers *api.HeaderAnalysis
|
||||||
RBL *DNSListResults
|
RBL *RBLResults
|
||||||
DNSWL *DNSListResults
|
|
||||||
SpamAssassin *api.SpamAssassinResult
|
SpamAssassin *api.SpamAssassinResult
|
||||||
Rspamd *api.RspamdResult
|
Rspamd *api.RspamdResult
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +81,6 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
|
||||||
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
|
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
|
||||||
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers)
|
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers)
|
||||||
results.RBL = r.rblChecker.CheckEmail(email)
|
results.RBL = r.rblChecker.CheckEmail(email)
|
||||||
results.DNSWL = r.dnswlChecker.CheckEmail(email)
|
|
||||||
results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email)
|
results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email)
|
||||||
results.Rspamd = r.rspamdAnalyzer.AnalyzeRspamd(email)
|
results.Rspamd = r.rspamdAnalyzer.AnalyzeRspamd(email)
|
||||||
results.Content = r.contentAnalyzer.AnalyzeContent(email)
|
results.Content = r.contentAnalyzer.AnalyzeContent(email)
|
||||||
|
|
@ -140,7 +135,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
||||||
blacklistScore := 0
|
blacklistScore := 0
|
||||||
var blacklistGrade string
|
var blacklistGrade string
|
||||||
if results.RBL != nil {
|
if results.RBL != nil {
|
||||||
blacklistScore, blacklistGrade = r.rblChecker.CalculateScore(results.RBL)
|
blacklistScore, blacklistGrade = r.rblChecker.CalculateRBLScore(results.RBL)
|
||||||
}
|
}
|
||||||
|
|
||||||
saScore, saGrade := r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin)
|
saScore, saGrade := r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin)
|
||||||
|
|
@ -202,11 +197,6 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
||||||
report.Blacklists = &results.RBL.Checks
|
report.Blacklists = &results.RBL.Checks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add whitelist checks as a map of IP -> array of BlacklistCheck (informational only)
|
|
||||||
if results.DNSWL != nil && len(results.DNSWL.Checks) > 0 {
|
|
||||||
report.Whitelists = &results.DNSWL.Checks
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add SpamAssassin result with individual deliverability score
|
// Add SpamAssassin result with individual deliverability score
|
||||||
if results.SpamAssassin != nil {
|
if results.SpamAssassin != nil {
|
||||||
saGradeTyped := api.SpamAssassinResultDeliverabilityGrade(saGrade)
|
saGradeTyped := api.SpamAssassinResultDeliverabilityGrade(saGrade)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewReportGenerator(t *testing.T) {
|
func TestNewReportGenerator(t *testing.T) {
|
||||||
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false)
|
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
|
||||||
if gen == nil {
|
if gen == nil {
|
||||||
t.Fatal("Expected report generator, got nil")
|
t.Fatal("Expected report generator, got nil")
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ func TestNewReportGenerator(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyzeEmail(t *testing.T) {
|
func TestAnalyzeEmail(t *testing.T) {
|
||||||
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false)
|
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
|
||||||
|
|
||||||
email := createTestEmail()
|
email := createTestEmail()
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ func TestAnalyzeEmail(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateReport(t *testing.T) {
|
func TestGenerateReport(t *testing.T) {
|
||||||
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false)
|
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
|
||||||
testID := uuid.New()
|
testID := uuid.New()
|
||||||
|
|
||||||
email := createTestEmail()
|
email := createTestEmail()
|
||||||
|
|
@ -130,7 +130,7 @@ func TestGenerateReport(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateReportWithSpamAssassin(t *testing.T) {
|
func TestGenerateReportWithSpamAssassin(t *testing.T) {
|
||||||
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false)
|
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
|
||||||
testID := uuid.New()
|
testID := uuid.New()
|
||||||
|
|
||||||
email := createTestEmailWithSpamAssassin()
|
email := createTestEmailWithSpamAssassin()
|
||||||
|
|
@ -150,7 +150,7 @@ func TestGenerateReportWithSpamAssassin(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateRawEmail(t *testing.T) {
|
func TestGenerateRawEmail(t *testing.T) {
|
||||||
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false)
|
gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@
|
||||||
highlight: { color: "good", bold: true },
|
highlight: { color: "good", bold: true },
|
||||||
link: "#dns-bimi",
|
link: "#dns-bimi",
|
||||||
});
|
});
|
||||||
if (bimiResult?.details && bimiResult.details.indexOf("declined") == 0) {
|
if (bimiResult.details && bimiResult.details.indexOf("declined") == 0) {
|
||||||
segments.push({ text: " declined to participate" });
|
segments.push({ text: " declined to participate" });
|
||||||
} else if (bimiResult?.result === "fail") {
|
} else if (bimiResult?.result === "fail") {
|
||||||
segments.push({ text: " but " });
|
segments.push({ text: " but " });
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { BlacklistCheck } from "$lib/api/types.gen";
|
|
||||||
import { theme } from "$lib/stores/theme";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
whitelists: Record<string, BlacklistCheck[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { whitelists }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card shadow-sm" id="dnswl-details">
|
|
||||||
<div class="card-header" class:bg-white={$theme === "light"} class:bg-dark={$theme !== "light"}>
|
|
||||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
|
||||||
<span>
|
|
||||||
<i class="bi bi-shield-check me-2"></i>
|
|
||||||
Whitelist Checks
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-info text-white">Informational</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
DNS whitelists identify trusted senders. Being listed here is a positive signal, but has
|
|
||||||
no impact on the overall score.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-lg-2">
|
|
||||||
{#each Object.entries(whitelists) as [ip, checks]}
|
|
||||||
<div class="col mb-3">
|
|
||||||
<h5 class="text-muted">
|
|
||||||
<i class="bi bi-hdd-network me-1"></i>
|
|
||||||
{ip}
|
|
||||||
</h5>
|
|
||||||
<table class="table table-sm table-striped table-hover mb-0">
|
|
||||||
<tbody>
|
|
||||||
{#each checks as check}
|
|
||||||
<tr>
|
|
||||||
<td title={check.response || "-"}>
|
|
||||||
<span
|
|
||||||
class="badge"
|
|
||||||
class:bg-success={check.listed}
|
|
||||||
class:bg-dark={check.error}
|
|
||||||
class:bg-secondary={!check.listed && !check.error}
|
|
||||||
>
|
|
||||||
{check.error
|
|
||||||
? "Error"
|
|
||||||
: check.listed
|
|
||||||
? "Listed"
|
|
||||||
: "Not listed"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td><code>{check.rbl}</code></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -24,4 +24,3 @@ export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
|
||||||
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
|
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
|
||||||
export { default as SummaryCard } from "./SummaryCard.svelte";
|
export { default as SummaryCard } from "./SummaryCard.svelte";
|
||||||
export { default as TinySurvey } from "./TinySurvey.svelte";
|
export { default as TinySurvey } from "./TinySurvey.svelte";
|
||||||
export { default as WhitelistCard } from "./WhitelistCard.svelte";
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ interface AppConfig {
|
||||||
report_retention?: number;
|
report_retention?: number;
|
||||||
survey_url?: string;
|
survey_url?: string;
|
||||||
custom_logo_url?: string;
|
custom_logo_url?: string;
|
||||||
rbls?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: AppConfig = {
|
const defaultConfig: AppConfig = {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ const getInitialTheme = () => {
|
||||||
if (!browser) return "light";
|
if (!browser) return "light";
|
||||||
|
|
||||||
const stored = localStorage.getItem("theme");
|
const stored = localStorage.getItem("theme");
|
||||||
if (stored === "light" || stored === "dark") return stored;
|
if (stored) return stored;
|
||||||
|
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.response.ok) {
|
if (response.response.ok) {
|
||||||
result = response.data ?? null;
|
result = response.data;
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
error = response.error.message || "Failed to check IP address";
|
error = response.error.message || "Failed to check IP address";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
<div class="d-flex justify-content-end me-lg-5 mt-3">
|
<div class="d-flex justify-content-end me-lg-5 mt-3">
|
||||||
<TinySurvey
|
<TinySurvey
|
||||||
class="bg-primary-subtle rounded-4 p-3 text-center"
|
class="bg-primary-subtle rounded-4 p-3 text-center"
|
||||||
source={"domain-" + result.domain}
|
source={"rbl-" + result.ip}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
import { getReport, getTest, reanalyzeReport } from "$lib/api";
|
import { getReport, getTest, reanalyzeReport } from "$lib/api";
|
||||||
import type { BlacklistCheck, Report, Test } from "$lib/api/types.gen";
|
import type { Report, Test } from "$lib/api/types.gen";
|
||||||
import {
|
import {
|
||||||
AuthenticationCard,
|
AuthenticationCard,
|
||||||
BlacklistCard,
|
BlacklistCard,
|
||||||
|
|
@ -17,11 +17,8 @@
|
||||||
SpamAssassinCard,
|
SpamAssassinCard,
|
||||||
SummaryCard,
|
SummaryCard,
|
||||||
TinySurvey,
|
TinySurvey,
|
||||||
WhitelistCard,
|
|
||||||
} from "$lib/components";
|
} from "$lib/components";
|
||||||
|
|
||||||
type BlacklistRecords = Record<string, BlacklistCheck[]>;
|
|
||||||
|
|
||||||
let testId = $derived(page.params.test);
|
let testId = $derived(page.params.test);
|
||||||
let test = $state<Test | null>(null);
|
let test = $state<Test | null>(null);
|
||||||
let report = $state<Report | null>(null);
|
let report = $state<Report | null>(null);
|
||||||
|
|
@ -324,46 +321,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Blacklist Checks -->
|
<!-- Blacklist Checks -->
|
||||||
{#snippet blacklistChecks(blacklists: BlacklistRecords, report: Report)}
|
{#if report.blacklists && Object.keys(report.blacklists).length > 0}
|
||||||
<BlacklistCard
|
<div class="row mb-4" id="blacklist">
|
||||||
{blacklists}
|
<div class="col-12">
|
||||||
blacklistGrade={report.summary?.blacklist_grade}
|
<BlacklistCard
|
||||||
blacklistScore={report.summary?.blacklist_score}
|
blacklists={report.blacklists}
|
||||||
receivedChain={report.header_analysis?.received_chain}
|
blacklistGrade={report.summary?.blacklist_grade}
|
||||||
/>
|
blacklistScore={report.summary?.blacklist_score}
|
||||||
{/snippet}
|
receivedChain={report.header_analysis?.received_chain}
|
||||||
|
/>
|
||||||
<!-- Whitelist Checks -->
|
|
||||||
{#snippet whitelistChecks(whitelists: BlacklistRecords)}
|
|
||||||
<WhitelistCard {whitelists} />
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<!-- Blacklist & Whitelist Checks -->
|
|
||||||
{#if report.blacklists && report.whitelists && Object.keys(report.blacklists).length == 1 && Object.keys(report.whitelists).length == 1}
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-6" id="blacklist">
|
|
||||||
{@render blacklistChecks(report.blacklists, report)}
|
|
||||||
</div>
|
|
||||||
<div class="col-6" id="whitelist">
|
|
||||||
{@render whitelistChecks(report.whitelists)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
{#if report.blacklists && Object.keys(report.blacklists).length > 0}
|
|
||||||
<div class="row mb-4" id="blacklist">
|
|
||||||
<div class="col-12">
|
|
||||||
{@render blacklistChecks(report.blacklists, report)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if report.whitelists && Object.keys(report.whitelists).length > 0}
|
|
||||||
<div class="row mb-4" id="whitelist">
|
|
||||||
<div class="col-12">
|
|
||||||
{@render whitelistChecks(report.whitelists)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Header Analysis -->
|
<!-- Header Analysis -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue