Implement ARC header check
This commit is contained in:
parent
c1211a8ce1
commit
433bfd9ee3
8 changed files with 325 additions and 75 deletions
|
|
@ -355,6 +355,8 @@ components:
|
|||
$ref: '#/components/schemas/AuthResult'
|
||||
bimi:
|
||||
$ref: '#/components/schemas/AuthResult'
|
||||
arc:
|
||||
$ref: '#/components/schemas/ARCResult'
|
||||
|
||||
AuthResult:
|
||||
type: object
|
||||
|
|
@ -378,6 +380,29 @@ components:
|
|||
type: string
|
||||
description: Additional details about the result
|
||||
|
||||
ARCResult:
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
enum: [pass, fail, none]
|
||||
description: Overall ARC chain validation result
|
||||
example: "pass"
|
||||
chain_valid:
|
||||
type: boolean
|
||||
description: Whether the ARC chain signatures are valid
|
||||
example: true
|
||||
chain_length:
|
||||
type: integer
|
||||
description: Number of ARC sets in the chain
|
||||
example: 2
|
||||
details:
|
||||
type: string
|
||||
description: Additional details about ARC validation
|
||||
example: "ARC chain valid with 2 intermediaries"
|
||||
|
||||
SpamAssassinResult:
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -59,6 +59,14 @@ func (a *AuthenticationAnalyzer) AnalyzeAuthentication(email *EmailMessage) *api
|
|||
}
|
||||
}
|
||||
|
||||
// Parse ARC headers if not already parsed from Authentication-Results
|
||||
if results.Arc == nil {
|
||||
results.Arc = a.parseARCHeaders(email)
|
||||
} else {
|
||||
// Enhance the ARC result with chain information from raw headers
|
||||
a.enhanceARCResult(email, results.Arc)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +119,13 @@ func (a *AuthenticationAnalyzer) parseAuthenticationResultsHeader(header string,
|
|||
results.Bimi = a.parseBIMIResult(part)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ARC
|
||||
if strings.HasPrefix(part, "arc=") {
|
||||
if results.Arc == nil {
|
||||
results.Arc = a.parseARCResult(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,6 +274,163 @@ func (a *AuthenticationAnalyzer) parseBIMIResult(part string) *api.AuthResult {
|
|||
return result
|
||||
}
|
||||
|
||||
// parseARCResult parses ARC result from Authentication-Results
|
||||
// Example: arc=pass
|
||||
func (a *AuthenticationAnalyzer) parseARCResult(part string) *api.ARCResult {
|
||||
result := &api.ARCResult{}
|
||||
|
||||
// Extract result (pass, fail, none)
|
||||
re := regexp.MustCompile(`arc=(\w+)`)
|
||||
if matches := re.FindStringSubmatch(part); len(matches) > 1 {
|
||||
resultStr := strings.ToLower(matches[1])
|
||||
result.Result = api.ARCResultResult(resultStr)
|
||||
}
|
||||
|
||||
// Extract details
|
||||
if idx := strings.Index(part, "("); idx != -1 {
|
||||
endIdx := strings.Index(part[idx:], ")")
|
||||
if endIdx != -1 {
|
||||
details := strings.TrimSpace(part[idx+1 : idx+endIdx])
|
||||
result.Details = &details
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseARCHeaders parses ARC headers from email message
|
||||
// ARC consists of three headers per hop: ARC-Authentication-Results, ARC-Message-Signature, ARC-Seal
|
||||
func (a *AuthenticationAnalyzer) parseARCHeaders(email *EmailMessage) *api.ARCResult {
|
||||
// Get all ARC-related headers
|
||||
arcAuthResults := email.Header[textprotoCanonical("ARC-Authentication-Results")]
|
||||
arcMessageSig := email.Header[textprotoCanonical("ARC-Message-Signature")]
|
||||
arcSeal := email.Header[textprotoCanonical("ARC-Seal")]
|
||||
|
||||
// If no ARC headers present, return nil
|
||||
if len(arcAuthResults) == 0 && len(arcMessageSig) == 0 && len(arcSeal) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &api.ARCResult{
|
||||
Result: api.ARCResultResultNone,
|
||||
}
|
||||
|
||||
// Count the ARC chain length (number of sets)
|
||||
chainLength := len(arcSeal)
|
||||
result.ChainLength = &chainLength
|
||||
|
||||
// Validate the ARC chain
|
||||
chainValid := a.validateARCChain(arcAuthResults, arcMessageSig, arcSeal)
|
||||
result.ChainValid = &chainValid
|
||||
|
||||
// Determine overall result
|
||||
if chainLength == 0 {
|
||||
result.Result = api.ARCResultResultNone
|
||||
details := "No ARC chain present"
|
||||
result.Details = &details
|
||||
} else if !chainValid {
|
||||
result.Result = api.ARCResultResultFail
|
||||
details := fmt.Sprintf("ARC chain validation failed (chain length: %d)", chainLength)
|
||||
result.Details = &details
|
||||
} else {
|
||||
result.Result = api.ARCResultResultPass
|
||||
details := fmt.Sprintf("ARC chain valid with %d intermediar%s", chainLength, pluralize(chainLength))
|
||||
result.Details = &details
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// enhanceARCResult enhances an existing ARC result with chain information
|
||||
func (a *AuthenticationAnalyzer) enhanceARCResult(email *EmailMessage, arcResult *api.ARCResult) {
|
||||
if arcResult == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get ARC headers
|
||||
arcSeal := email.Header[textprotoCanonical("ARC-Seal")]
|
||||
arcMessageSig := email.Header[textprotoCanonical("ARC-Message-Signature")]
|
||||
arcAuthResults := email.Header[textprotoCanonical("ARC-Authentication-Results")]
|
||||
|
||||
// Set chain length if not already set
|
||||
if arcResult.ChainLength == nil {
|
||||
chainLength := len(arcSeal)
|
||||
arcResult.ChainLength = &chainLength
|
||||
}
|
||||
|
||||
// Validate chain if not already validated
|
||||
if arcResult.ChainValid == nil {
|
||||
chainValid := a.validateARCChain(arcAuthResults, arcMessageSig, arcSeal)
|
||||
arcResult.ChainValid = &chainValid
|
||||
}
|
||||
}
|
||||
|
||||
// validateARCChain validates the ARC chain for completeness
|
||||
// Each instance should have all three headers with matching instance numbers
|
||||
func (a *AuthenticationAnalyzer) validateARCChain(arcAuthResults, arcMessageSig, arcSeal []string) bool {
|
||||
// All three header types should have the same count
|
||||
if len(arcAuthResults) != len(arcMessageSig) || len(arcAuthResults) != len(arcSeal) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(arcSeal) == 0 {
|
||||
return true // No ARC chain is technically valid
|
||||
}
|
||||
|
||||
// Extract instance numbers from each header type
|
||||
sealInstances := a.extractARCInstances(arcSeal)
|
||||
sigInstances := a.extractARCInstances(arcMessageSig)
|
||||
authInstances := a.extractARCInstances(arcAuthResults)
|
||||
|
||||
// Check that all instance numbers match and are sequential starting from 1
|
||||
if len(sealInstances) != len(sigInstances) || len(sealInstances) != len(authInstances) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify instances are sequential from 1 to N
|
||||
for i := 1; i <= len(sealInstances); i++ {
|
||||
if !contains(sealInstances, i) || !contains(sigInstances, i) || !contains(authInstances, i) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// extractARCInstances extracts instance numbers from ARC headers
|
||||
func (a *AuthenticationAnalyzer) extractARCInstances(headers []string) []int {
|
||||
var instances []int
|
||||
re := regexp.MustCompile(`i=(\d+)`)
|
||||
|
||||
for _, header := range headers {
|
||||
if matches := re.FindStringSubmatch(header); len(matches) > 1 {
|
||||
var instance int
|
||||
fmt.Sscanf(matches[1], "%d", &instance)
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
}
|
||||
|
||||
return instances
|
||||
}
|
||||
|
||||
// contains checks if a slice contains an integer
|
||||
func contains(slice []int, val int) bool {
|
||||
for _, item := range slice {
|
||||
if item == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pluralize returns "y" or "ies" based on count
|
||||
func pluralize(count int) string {
|
||||
if count == 1 {
|
||||
return "y"
|
||||
}
|
||||
return "ies"
|
||||
}
|
||||
|
||||
// parseLegacySPF attempts to parse SPF from Received-SPF header
|
||||
func (a *AuthenticationAnalyzer) parseLegacySPF(email *EmailMessage) *api.AuthResult {
|
||||
receivedSPF := email.Header.Get("Received-SPF")
|
||||
|
|
@ -389,7 +561,7 @@ func (a *AuthenticationAnalyzer) GenerateAuthenticationChecks(results *api.Authe
|
|||
Status: api.CheckStatusWarn,
|
||||
Score: 0.0,
|
||||
Message: "No SPF authentication result found",
|
||||
Severity: api.PtrTo(api.Medium),
|
||||
Severity: api.PtrTo(api.CheckSeverityMedium),
|
||||
Advice: api.PtrTo("Ensure your MTA is configured to check SPF records"),
|
||||
})
|
||||
}
|
||||
|
|
@ -407,7 +579,7 @@ func (a *AuthenticationAnalyzer) GenerateAuthenticationChecks(results *api.Authe
|
|||
Status: api.CheckStatusWarn,
|
||||
Score: 0.0,
|
||||
Message: "No DKIM signature found",
|
||||
Severity: api.PtrTo(api.Medium),
|
||||
Severity: api.PtrTo(api.CheckSeverityMedium),
|
||||
Advice: api.PtrTo("Configure DKIM signing for your domain to improve deliverability"),
|
||||
})
|
||||
}
|
||||
|
|
@ -423,7 +595,7 @@ func (a *AuthenticationAnalyzer) GenerateAuthenticationChecks(results *api.Authe
|
|||
Status: api.CheckStatusWarn,
|
||||
Score: 0.0,
|
||||
Message: "No DMARC authentication result found",
|
||||
Severity: api.PtrTo(api.Medium),
|
||||
Severity: api.PtrTo(api.CheckSeverityMedium),
|
||||
Advice: api.PtrTo("Implement DMARC policy for your domain"),
|
||||
})
|
||||
}
|
||||
|
|
@ -434,6 +606,12 @@ func (a *AuthenticationAnalyzer) GenerateAuthenticationChecks(results *api.Authe
|
|||
checks = append(checks, check)
|
||||
}
|
||||
|
||||
// ARC check (optional, for forwarded emails)
|
||||
if results.Arc != nil {
|
||||
check := a.generateARCCheck(results.Arc)
|
||||
checks = append(checks, check)
|
||||
}
|
||||
|
||||
return checks
|
||||
}
|
||||
|
||||
|
|
@ -448,31 +626,31 @@ func (a *AuthenticationAnalyzer) generateSPFCheck(spf *api.AuthResult) api.Check
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = "SPF validation passed"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your SPF record is properly configured")
|
||||
case api.AuthResultResultFail:
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = "SPF validation failed"
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
check.Advice = api.PtrTo("Fix your SPF record to authorize this sending server")
|
||||
case api.AuthResultResultSoftfail:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.5
|
||||
check.Message = "SPF validation softfail"
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Review your SPF record configuration")
|
||||
case api.AuthResultResultNeutral:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.5
|
||||
check.Message = "SPF validation neutral"
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("Consider tightening your SPF policy")
|
||||
default:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("SPF validation result: %s", spf.Result)
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Review your SPF record configuration")
|
||||
}
|
||||
|
||||
|
|
@ -495,19 +673,19 @@ func (a *AuthenticationAnalyzer) generateDKIMCheck(dkim *api.AuthResult, index i
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = "DKIM signature is valid"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your DKIM signature is properly configured")
|
||||
case api.AuthResultResultFail:
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = "DKIM signature validation failed"
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Check your DKIM keys and signing configuration")
|
||||
default:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("DKIM validation result: %s", dkim.Result)
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Ensure DKIM signing is enabled and configured correctly")
|
||||
}
|
||||
|
||||
|
|
@ -537,19 +715,19 @@ func (a *AuthenticationAnalyzer) generateDMARCCheck(dmarc *api.AuthResult) api.C
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = "DMARC validation passed"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your DMARC policy is properly aligned")
|
||||
case api.AuthResultResultFail:
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = "DMARC validation failed"
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Ensure SPF or DKIM alignment with your From domain")
|
||||
default:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("DMARC validation result: %s", dmarc.Result)
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Configure DMARC policy for your domain")
|
||||
}
|
||||
|
||||
|
|
@ -572,19 +750,19 @@ func (a *AuthenticationAnalyzer) generateBIMICheck(bimi *api.AuthResult) api.Che
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.0 // BIMI doesn't contribute to score (branding feature)
|
||||
check.Message = "BIMI validation passed"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your brand logo is properly configured via BIMI")
|
||||
case api.AuthResultResultFail:
|
||||
check.Status = api.CheckStatusInfo
|
||||
check.Score = 0.0
|
||||
check.Message = "BIMI validation failed"
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("BIMI is optional but can improve brand recognition. Ensure DMARC is enforced (p=quarantine or p=reject) and configure a valid BIMI record")
|
||||
default:
|
||||
check.Status = api.CheckStatusInfo
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("BIMI validation result: %s", bimi.Result)
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("BIMI is optional. Consider implementing it to display your brand logo in supported email clients")
|
||||
}
|
||||
|
||||
|
|
@ -595,3 +773,50 @@ func (a *AuthenticationAnalyzer) generateBIMICheck(bimi *api.AuthResult) api.Che
|
|||
|
||||
return check
|
||||
}
|
||||
|
||||
func (a *AuthenticationAnalyzer) generateARCCheck(arc *api.ARCResult) api.Check {
|
||||
check := api.Check{
|
||||
Category: api.Authentication,
|
||||
Name: "ARC (Authenticated Received Chain)",
|
||||
}
|
||||
|
||||
switch arc.Result {
|
||||
case api.ARCResultResultPass:
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.0 // ARC doesn't contribute to score (informational for forwarding)
|
||||
check.Message = "ARC chain validation passed"
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("ARC preserves authentication results through email forwarding. Your email passed through intermediaries while maintaining authentication")
|
||||
case api.ARCResultResultFail:
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Message = "ARC chain validation failed"
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("The ARC chain is broken or invalid. This may indicate issues with email forwarding intermediaries")
|
||||
default:
|
||||
check.Status = api.CheckStatusInfo
|
||||
check.Score = 0.0
|
||||
check.Message = "No ARC chain present"
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("ARC is not present. This is normal for emails sent directly without forwarding through mailing lists or other intermediaries")
|
||||
}
|
||||
|
||||
// Build details
|
||||
var detailsParts []string
|
||||
if arc.ChainLength != nil {
|
||||
detailsParts = append(detailsParts, fmt.Sprintf("Chain length: %d", *arc.ChainLength))
|
||||
}
|
||||
if arc.ChainValid != nil {
|
||||
detailsParts = append(detailsParts, fmt.Sprintf("Chain valid: %v", *arc.ChainValid))
|
||||
}
|
||||
if arc.Details != nil {
|
||||
detailsParts = append(detailsParts, *arc.Details)
|
||||
}
|
||||
|
||||
if len(detailsParts) > 0 {
|
||||
details := strings.Join(detailsParts, ", ")
|
||||
check.Details = &details
|
||||
}
|
||||
|
||||
return check
|
||||
}
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ func (c *ContentAnalyzer) generateHTMLValidityCheck(results *ContentResults) api
|
|||
if !results.HTMLValid {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = "HTML structure is invalid"
|
||||
if len(results.HTMLErrors) > 0 {
|
||||
details := strings.Join(results.HTMLErrors, "; ")
|
||||
|
|
@ -517,7 +517,7 @@ func (c *ContentAnalyzer) generateHTMLValidityCheck(results *ContentResults) api
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.2
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "HTML structure is valid"
|
||||
check.Advice = api.PtrTo("Your HTML is well-formed")
|
||||
}
|
||||
|
|
@ -552,7 +552,7 @@ func (c *ContentAnalyzer) generateLinkChecks(results *ContentResults) []api.Chec
|
|||
if brokenLinks > 0 {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Message = fmt.Sprintf("Found %d broken link(s)", brokenLinks)
|
||||
check.Advice = api.PtrTo("Fix or remove broken links to improve deliverability")
|
||||
details := fmt.Sprintf("Total links: %d, Broken: %d", len(results.Links), brokenLinks)
|
||||
|
|
@ -560,7 +560,7 @@ func (c *ContentAnalyzer) generateLinkChecks(results *ContentResults) []api.Chec
|
|||
} else if warningLinks > 0 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = fmt.Sprintf("Found %d link(s) that could not be verified", warningLinks)
|
||||
check.Advice = api.PtrTo("Review links that could not be verified")
|
||||
details := fmt.Sprintf("Total links: %d, Unverified: %d", len(results.Links), warningLinks)
|
||||
|
|
@ -568,7 +568,7 @@ func (c *ContentAnalyzer) generateLinkChecks(results *ContentResults) []api.Chec
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.4
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = fmt.Sprintf("All %d link(s) are valid", len(results.Links))
|
||||
check.Advice = api.PtrTo("Your links are working properly")
|
||||
}
|
||||
|
|
@ -601,7 +601,7 @@ func (c *ContentAnalyzer) generateImageChecks(results *ContentResults) []api.Che
|
|||
if noAltCount == len(results.Images) {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = "No images have alt attributes"
|
||||
check.Advice = api.PtrTo("Add alt text to all images for accessibility and deliverability")
|
||||
details := fmt.Sprintf("Images without alt: %d/%d", noAltCount, len(results.Images))
|
||||
|
|
@ -609,7 +609,7 @@ func (c *ContentAnalyzer) generateImageChecks(results *ContentResults) []api.Che
|
|||
} else if noAltCount > 0 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.2
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = fmt.Sprintf("%d image(s) missing alt attributes", noAltCount)
|
||||
check.Advice = api.PtrTo("Add alt text to all images for better accessibility")
|
||||
details := fmt.Sprintf("Images without alt: %d/%d", noAltCount, len(results.Images))
|
||||
|
|
@ -617,7 +617,7 @@ func (c *ContentAnalyzer) generateImageChecks(results *ContentResults) []api.Che
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "All images have alt attributes"
|
||||
check.Advice = api.PtrTo("Your images are properly tagged for accessibility")
|
||||
}
|
||||
|
|
@ -636,13 +636,13 @@ func (c *ContentAnalyzer) generateUnsubscribeCheck(results *ContentResults) api.
|
|||
if !results.HasUnsubscribe {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = "No unsubscribe link found"
|
||||
check.Advice = api.PtrTo("Add an unsubscribe link for marketing emails (RFC 8058)")
|
||||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = fmt.Sprintf("Found %d unsubscribe link(s)", len(results.UnsubscribeLinks))
|
||||
check.Advice = api.PtrTo("Your email includes an unsubscribe option")
|
||||
}
|
||||
|
|
@ -662,7 +662,7 @@ func (c *ContentAnalyzer) generateTextConsistencyCheck(results *ContentResults)
|
|||
if consistency < 0.3 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = "Plain text and HTML versions differ significantly"
|
||||
check.Advice = api.PtrTo("Ensure plain text and HTML versions convey the same content")
|
||||
details := fmt.Sprintf("Consistency: %.0f%%", consistency*100)
|
||||
|
|
@ -670,7 +670,7 @@ func (c *ContentAnalyzer) generateTextConsistencyCheck(results *ContentResults)
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "Plain text and HTML versions are consistent"
|
||||
check.Advice = api.PtrTo("Your multipart email is well-structured")
|
||||
details := fmt.Sprintf("Consistency: %.0f%%", consistency*100)
|
||||
|
|
@ -693,7 +693,7 @@ func (c *ContentAnalyzer) generateImageRatioCheck(results *ContentResults) api.C
|
|||
if ratio > 10.0 {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = "Email is excessively image-heavy"
|
||||
check.Advice = api.PtrTo("Reduce the number of images relative to text content")
|
||||
details := fmt.Sprintf("Images: %d, Ratio: %.2f images per 1000 chars", len(results.Images), ratio)
|
||||
|
|
@ -701,7 +701,7 @@ func (c *ContentAnalyzer) generateImageRatioCheck(results *ContentResults) api.C
|
|||
} else if ratio > 5.0 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.2
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = "Email has high image-to-text ratio"
|
||||
check.Advice = api.PtrTo("Consider adding more text content relative to images")
|
||||
details := fmt.Sprintf("Images: %d, Ratio: %.2f images per 1000 chars", len(results.Images), ratio)
|
||||
|
|
@ -709,7 +709,7 @@ func (c *ContentAnalyzer) generateImageRatioCheck(results *ContentResults) api.C
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "Image-to-text ratio is reasonable"
|
||||
check.Advice = api.PtrTo("Your content has a good balance of images and text")
|
||||
details := fmt.Sprintf("Images: %d, Ratio: %.2f images per 1000 chars", len(results.Images), ratio)
|
||||
|
|
@ -730,7 +730,7 @@ func (c *ContentAnalyzer) generateSuspiciousURLCheck(results *ContentResults) ap
|
|||
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = fmt.Sprintf("Found %d suspicious URL(s)", count)
|
||||
check.Advice = api.PtrTo("Avoid URL shorteners, IP addresses, and obfuscated URLs in emails")
|
||||
|
||||
|
|
|
|||
|
|
@ -537,7 +537,7 @@ func (d *DNSAnalyzer) generateMXCheck(results *DNSResults) api.Check {
|
|||
if len(results.MXRecords) == 0 || !results.MXRecords[0].Valid {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
|
||||
if len(results.MXRecords) > 0 && results.MXRecords[0].Error != "" {
|
||||
check.Message = results.MXRecords[0].Error
|
||||
|
|
@ -548,7 +548,7 @@ func (d *DNSAnalyzer) generateMXCheck(results *DNSResults) api.Check {
|
|||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = fmt.Sprintf("Found %d valid MX record(s)", len(results.MXRecords))
|
||||
|
||||
// Add details about MX records
|
||||
|
|
@ -577,14 +577,14 @@ func (d *DNSAnalyzer) generateSPFCheck(spf *SPFRecord) api.Check {
|
|||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = spf.Error
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Configure an SPF record for your domain to improve deliverability")
|
||||
} else {
|
||||
// If record exists but is invalid, it's a warning
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.5
|
||||
check.Message = "SPF record found but appears invalid"
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Review and fix your SPF record syntax")
|
||||
check.Details = &spf.Record
|
||||
}
|
||||
|
|
@ -592,7 +592,7 @@ func (d *DNSAnalyzer) generateSPFCheck(spf *SPFRecord) api.Check {
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = "Valid SPF record found"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Details = &spf.Record
|
||||
check.Advice = api.PtrTo("Your SPF record is properly configured")
|
||||
}
|
||||
|
|
@ -611,7 +611,7 @@ func (d *DNSAnalyzer) generateDKIMCheck(dkim *DKIMRecord) api.Check {
|
|||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("DKIM record not found or invalid: %s", dkim.Error)
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Ensure DKIM record is published in DNS for the selector used")
|
||||
details := fmt.Sprintf("Selector: %s, Domain: %s", dkim.Selector, dkim.Domain)
|
||||
check.Details = &details
|
||||
|
|
@ -619,7 +619,7 @@ func (d *DNSAnalyzer) generateDKIMCheck(dkim *DKIMRecord) api.Check {
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = "Valid DKIM record found"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
details := fmt.Sprintf("Selector: %s, Domain: %s", dkim.Selector, dkim.Domain)
|
||||
check.Details = &details
|
||||
check.Advice = api.PtrTo("Your DKIM record is properly published")
|
||||
|
|
@ -639,13 +639,13 @@ func (d *DNSAnalyzer) generateDMARCCheck(dmarc *DMARCRecord) api.Check {
|
|||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Message = dmarc.Error
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Configure a DMARC record for your domain to improve deliverability and prevent spoofing")
|
||||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Message = fmt.Sprintf("Valid DMARC record found with policy: %s", dmarc.Policy)
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Details = &dmarc.Record
|
||||
|
||||
// Provide advice based on policy
|
||||
|
|
@ -681,14 +681,14 @@ func (d *DNSAnalyzer) generateBIMICheck(bimi *BIMIRecord) api.Check {
|
|||
check.Status = api.CheckStatusInfo
|
||||
check.Score = 0.0
|
||||
check.Message = "No BIMI record found (optional)"
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("BIMI is optional. Consider implementing it to display your brand logo in supported email clients. Requires enforced DMARC policy (p=quarantine or p=reject)")
|
||||
} else {
|
||||
// If record exists but is invalid
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("BIMI record found but invalid: %s", bimi.Error)
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Advice = api.PtrTo("Review and fix your BIMI record syntax. Ensure it contains v=BIMI1 and a valid logo URL (l=)")
|
||||
check.Details = &bimi.Record
|
||||
}
|
||||
|
|
@ -696,7 +696,7 @@ func (d *DNSAnalyzer) generateBIMICheck(bimi *BIMIRecord) api.Check {
|
|||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.0 // BIMI doesn't contribute to score (branding feature)
|
||||
check.Message = "Valid BIMI record found"
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
|
||||
// Build details with logo and VMC URLs
|
||||
var detailsParts []string
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ func (r *RBLChecker) GenerateRBLChecks(results *RBLResults) []api.Check {
|
|||
Status: api.CheckStatusWarn,
|
||||
Score: 1.0,
|
||||
Message: "No public IP addresses found to check",
|
||||
Severity: api.PtrTo(api.Low),
|
||||
Severity: api.PtrTo(api.CheckSeverityLow),
|
||||
Advice: api.PtrTo("Unable to extract sender IP from email headers"),
|
||||
})
|
||||
return checks
|
||||
|
|
@ -316,22 +316,22 @@ func (r *RBLChecker) generateSummaryCheck(results *RBLResults) api.Check {
|
|||
if listedCount == 0 {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Message = fmt.Sprintf("Not listed on any blacklists (%d RBLs checked)", len(r.RBLs))
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your sending IP has a good reputation")
|
||||
} else if listedCount == 1 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Message = fmt.Sprintf("Listed on 1 blacklist (out of %d checked)", totalChecks)
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("You're listed on one blacklist. Review the specific listing and request delisting if appropriate")
|
||||
} else if listedCount <= 3 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Message = fmt.Sprintf("Listed on %d blacklists (out of %d checked)", listedCount, totalChecks)
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Multiple blacklist listings detected. This will significantly impact deliverability. Review each listing and take corrective action")
|
||||
} else {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Message = fmt.Sprintf("Listed on %d blacklists (out of %d checked)", listedCount, totalChecks)
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
check.Advice = api.PtrTo("Your IP is listed on multiple blacklists. This will severely impact email deliverability. Investigate the cause and request delisting from each RBL")
|
||||
}
|
||||
|
||||
|
|
@ -357,15 +357,15 @@ func (r *RBLChecker) generateListingCheck(rblCheck *RBLCheck) api.Check {
|
|||
|
||||
// Determine severity based on which RBL
|
||||
if strings.Contains(rblCheck.RBL, "spamhaus") {
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
advice := fmt.Sprintf("Listed on Spamhaus, a widely-used blocklist. Visit https://check.spamhaus.org/ to check details and request delisting")
|
||||
check.Advice = &advice
|
||||
} else if strings.Contains(rblCheck.RBL, "spamcop") {
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
advice := fmt.Sprintf("Listed on SpamCop. Visit http://www.spamcop.net/bl.shtml to request delisting")
|
||||
check.Advice = &advice
|
||||
} else {
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
advice := fmt.Sprintf("Listed on %s. Contact the RBL operator for delisting procedures", rblCheck.RBL)
|
||||
check.Advice = &advice
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ func TestGenerateListingCheck(t *testing.T) {
|
|||
Response: "127.0.0.2",
|
||||
},
|
||||
expectedStatus: api.CheckStatusFail,
|
||||
expectedSeverity: api.Critical,
|
||||
expectedSeverity: api.CheckSeverityCritical,
|
||||
},
|
||||
{
|
||||
name: "SpamCop listing",
|
||||
|
|
@ -430,7 +430,7 @@ func TestGenerateListingCheck(t *testing.T) {
|
|||
Response: "127.0.0.2",
|
||||
},
|
||||
expectedStatus: api.CheckStatusFail,
|
||||
expectedSeverity: api.High,
|
||||
expectedSeverity: api.CheckSeverityHigh,
|
||||
},
|
||||
{
|
||||
name: "Other RBL listing",
|
||||
|
|
@ -441,7 +441,7 @@ func TestGenerateListingCheck(t *testing.T) {
|
|||
Response: "127.0.0.2",
|
||||
},
|
||||
expectedStatus: api.CheckStatusFail,
|
||||
expectedSeverity: api.High,
|
||||
expectedSeverity: api.CheckSeverityHigh,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -351,13 +351,13 @@ func (s *DeliverabilityScorer) generateRequiredHeadersCheck(email *EmailMessage)
|
|||
if len(missing) == 0 {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.4
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "All required headers are present"
|
||||
check.Advice = api.PtrTo("Your email has proper RFC 5322 headers")
|
||||
} else {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
check.Message = fmt.Sprintf("Missing required header(s): %s", strings.Join(missing, ", "))
|
||||
check.Advice = api.PtrTo("Add all required headers to ensure email deliverability")
|
||||
details := fmt.Sprintf("Missing: %s", strings.Join(missing, ", "))
|
||||
|
|
@ -386,13 +386,13 @@ func (s *DeliverabilityScorer) generateRecommendedHeadersCheck(email *EmailMessa
|
|||
if len(missing) == 0 {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.3
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "All recommended headers are present"
|
||||
check.Advice = api.PtrTo("Your email includes all recommended headers")
|
||||
} else if len(missing) < len(recommendedHeaders) {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.15
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = fmt.Sprintf("Missing some recommended header(s): %s", strings.Join(missing, ", "))
|
||||
check.Advice = api.PtrTo("Consider adding recommended headers for better deliverability")
|
||||
details := fmt.Sprintf("Missing: %s", strings.Join(missing, ", "))
|
||||
|
|
@ -400,7 +400,7 @@ func (s *DeliverabilityScorer) generateRecommendedHeadersCheck(email *EmailMessa
|
|||
} else {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = "Missing all recommended headers"
|
||||
check.Advice = api.PtrTo("Add recommended headers (Subject, To, Reply-To) for better email presentation")
|
||||
}
|
||||
|
|
@ -420,20 +420,20 @@ func (s *DeliverabilityScorer) generateMessageIDCheck(email *EmailMessage) api.C
|
|||
if messageID == "" {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Message = "Message-ID header is missing"
|
||||
check.Advice = api.PtrTo("Add a unique Message-ID header to your email")
|
||||
} else if !s.isValidMessageID(messageID) {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.05
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Message = "Message-ID format is invalid"
|
||||
check.Advice = api.PtrTo("Use proper Message-ID format: <unique-id@domain.com>")
|
||||
check.Details = &messageID
|
||||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.1
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = "Message-ID is properly formatted"
|
||||
check.Advice = api.PtrTo("Your Message-ID follows RFC 5322 standards")
|
||||
check.Details = &messageID
|
||||
|
|
@ -452,13 +452,13 @@ func (s *DeliverabilityScorer) generateMIMEStructureCheck(email *EmailMessage) a
|
|||
if len(email.Parts) == 0 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Score = 0.0
|
||||
check.Severity = api.PtrTo(api.Low)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityLow)
|
||||
check.Message = "No MIME parts detected"
|
||||
check.Advice = api.PtrTo("Consider using multipart MIME for better compatibility")
|
||||
} else {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 0.2
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = fmt.Sprintf("Proper MIME structure with %d part(s)", len(email.Parts))
|
||||
check.Advice = api.PtrTo("Your email has proper MIME structure")
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ func (a *SpamAssassinAnalyzer) GenerateSpamAssassinChecks(result *SpamAssassinRe
|
|||
Status: api.CheckStatusWarn,
|
||||
Score: 0.0,
|
||||
Message: "No SpamAssassin headers found",
|
||||
Severity: api.PtrTo(api.Medium),
|
||||
Severity: api.PtrTo(api.CheckSeverityMedium),
|
||||
Advice: api.PtrTo("Ensure your MTA is configured to run SpamAssassin checks"),
|
||||
})
|
||||
return checks
|
||||
|
|
@ -260,27 +260,27 @@ func (a *SpamAssassinAnalyzer) generateMainSpamCheck(result *SpamAssassinResult)
|
|||
if score <= 0 {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Message = fmt.Sprintf("Excellent spam score: %.1f (threshold: %.1f)", score, required)
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your email has a negative spam score, indicating good email practices")
|
||||
} else if score < required {
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Message = fmt.Sprintf("Good spam score: %.1f (threshold: %.1f)", score, required)
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Advice = api.PtrTo("Your email passes spam filters")
|
||||
} else if score < required*1.5 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Message = fmt.Sprintf("Borderline spam score: %.1f (threshold: %.1f)", score, required)
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
check.Advice = api.PtrTo("Your email is close to being marked as spam. Review the triggered spam tests below")
|
||||
} else if score < required*2 {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Message = fmt.Sprintf("High spam score: %.1f (threshold: %.1f)", score, required)
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
check.Advice = api.PtrTo("Your email is likely to be marked as spam. Address the issues identified in spam tests")
|
||||
} else {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Message = fmt.Sprintf("Very high spam score: %.1f (threshold: %.1f)", score, required)
|
||||
check.Severity = api.PtrTo(api.Critical)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityCritical)
|
||||
check.Advice = api.PtrTo("Your email will almost certainly be marked as spam. Urgently address the spam test failures")
|
||||
}
|
||||
|
||||
|
|
@ -307,10 +307,10 @@ func (a *SpamAssassinAnalyzer) generateTestCheck(detail SpamTestDetail) api.Chec
|
|||
// Negative indicator (increases spam score)
|
||||
if detail.Score > 2.0 {
|
||||
check.Status = api.CheckStatusFail
|
||||
check.Severity = api.PtrTo(api.High)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityHigh)
|
||||
} else {
|
||||
check.Status = api.CheckStatusWarn
|
||||
check.Severity = api.PtrTo(api.Medium)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityMedium)
|
||||
}
|
||||
check.Score = 0.0
|
||||
check.Message = fmt.Sprintf("Test failed with score +%.1f", detail.Score)
|
||||
|
|
@ -320,7 +320,7 @@ func (a *SpamAssassinAnalyzer) generateTestCheck(detail SpamTestDetail) api.Chec
|
|||
// Positive indicator (decreases spam score)
|
||||
check.Status = api.CheckStatusPass
|
||||
check.Score = 1.0
|
||||
check.Severity = api.PtrTo(api.Info)
|
||||
check.Severity = api.PtrTo(api.CheckSeverityInfo)
|
||||
check.Message = fmt.Sprintf("Test passed with score %.1f", detail.Score)
|
||||
advice := fmt.Sprintf("%s. This test reduces your spam score by %.1f", detail.Description, -detail.Score)
|
||||
check.Advice = &advice
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue