diff --git a/internal/config/cli.go b/internal/config/cli.go index 17f0ff6..2a61bad 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -37,6 +37,7 @@ func declareFlags(o *Config) { flag.DurationVar(&o.Analysis.DNSTimeout, "dns-timeout", o.Analysis.DNSTimeout, "Timeout when performing DNS query") flag.DurationVar(&o.Analysis.HTTPTimeout, "http-timeout", o.Analysis.HTTPTimeout, "Timeout when performing HTTP query") flag.Var(&StringArray{&o.Analysis.RBLs}, "rbl", "Append a RBL (use this option multiple time to append multiple RBLs)") + flag.BoolVar(&o.Analysis.CheckAllIPs, "check-all-ips", o.Analysis.CheckAllIPs, "Check all IPs found in email headers against RBLs (not just the first one)") flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever") flag.UintVar(&o.RateLimit, "rate-limit", o.RateLimit, "API rate limit (requests per second per IP)") flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey") diff --git a/internal/config/config.go b/internal/config/config.go index 6f35ec7..be5e63a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,9 +61,10 @@ type EmailConfig struct { // AnalysisConfig contains timeout and behavior settings for email analysis type AnalysisConfig struct { - DNSTimeout time.Duration - HTTPTimeout time.Duration - RBLs []string + DNSTimeout time.Duration + HTTPTimeout time.Duration + RBLs []string + CheckAllIPs bool // Check all IPs found in headers, not just the first one } // DefaultConfig returns a configuration with sensible defaults @@ -86,6 +87,7 @@ func DefaultConfig() *Config { DNSTimeout: 5 * time.Second, HTTPTimeout: 10 * time.Second, RBLs: []string{}, + CheckAllIPs: false, // By default, only check the first IP }, } } diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 80fa7f2..99b7b52 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -44,6 +44,7 @@ func NewEmailAnalyzer(cfg *config.Config) *EmailAnalyzer { cfg.Analysis.DNSTimeout, cfg.Analysis.HTTPTimeout, cfg.Analysis.RBLs, + cfg.Analysis.CheckAllIPs, ) return &EmailAnalyzer{ diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index 613e5d5..3150d50 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -751,7 +751,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s brokenLinks++ } } - score += 20 * brokenLinks / len(results.Links) + score += 20 * (len(results.Links) - brokenLinks) / len(results.Links) // Too much links, 10 points penalty if len(results.Links) > 30 { score -= 10 @@ -769,7 +769,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s noAltCount++ } } - score += 15 * noAltCount / len(results.Images) + score += 15 * (len(results.Images) - noAltCount) / len(results.Images) } else { // No images is Ok score += 15 diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 832c61c..5e8b503 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -34,9 +34,10 @@ import ( // RBLChecker checks IP addresses against DNS-based blacklists type RBLChecker struct { - Timeout time.Duration - RBLs []string - resolver *net.Resolver + Timeout time.Duration + RBLs []string + CheckAllIPs bool // Check all IPs found in headers, not just the first one + resolver *net.Resolver } // DefaultRBLs is a list of commonly used RBL providers @@ -50,7 +51,7 @@ var DefaultRBLs = []string{ } // NewRBLChecker creates a new RBL checker with configurable timeout and RBL list -func NewRBLChecker(timeout time.Duration, rbls []string) *RBLChecker { +func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLChecker { if timeout == 0 { timeout = 5 * time.Second // Default timeout } @@ -58,8 +59,9 @@ func NewRBLChecker(timeout time.Duration, rbls []string) *RBLChecker { rbls = DefaultRBLs } return &RBLChecker{ - Timeout: timeout, - RBLs: rbls, + Timeout: timeout, + RBLs: rbls, + CheckAllIPs: checkAllIPs, resolver: &net.Resolver{ PreferGo: true, }, @@ -96,6 +98,11 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { results.ListedCount++ } } + + // Only check the first IP unless CheckAllIPs is enabled + if !r.CheckAllIPs { + break + } } return results diff --git a/pkg/analyzer/rbl_test.go b/pkg/analyzer/rbl_test.go index f18464a..a1de270 100644 --- a/pkg/analyzer/rbl_test.go +++ b/pkg/analyzer/rbl_test.go @@ -55,7 +55,7 @@ func TestNewRBLChecker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - checker := NewRBLChecker(tt.timeout, tt.rbls) + checker := NewRBLChecker(tt.timeout, tt.rbls, false) if checker.Timeout != tt.expectedTimeout { t.Errorf("Timeout = %v, want %v", checker.Timeout, tt.expectedTimeout) } @@ -97,7 +97,7 @@ func TestReverseIP(t *testing.T) { }, } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -157,7 +157,7 @@ func TestIsPublicIP(t *testing.T) { }, } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -237,7 +237,7 @@ func TestExtractIPs(t *testing.T) { },*/ } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -322,7 +322,7 @@ func TestGetBlacklistScore(t *testing.T) { }, } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -351,7 +351,7 @@ func TestGetUniqueListedIPs(t *testing.T) { }, } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) listedIPs := checker.GetUniqueListedIPs(results) expectedIPs := []string{"198.51.100.1", "198.51.100.2"} @@ -376,7 +376,7 @@ func TestGetRBLsForIP(t *testing.T) { }, } - checker := NewRBLChecker(5*time.Second, nil) + checker := NewRBLChecker(5*time.Second, nil, false) tests := []struct { name string diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go index bd6b866..a39a98a 100644 --- a/pkg/analyzer/report.go +++ b/pkg/analyzer/report.go @@ -44,12 +44,13 @@ func NewReportGenerator( dnsTimeout time.Duration, httpTimeout time.Duration, rbls []string, + checkAllIPs bool, ) *ReportGenerator { return &ReportGenerator{ authAnalyzer: NewAuthenticationAnalyzer(), spamAnalyzer: NewSpamAssassinAnalyzer(), dnsAnalyzer: NewDNSAnalyzer(dnsTimeout), - rblChecker: NewRBLChecker(dnsTimeout, rbls), + rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs), contentAnalyzer: NewContentAnalyzer(httpTimeout), headerAnalyzer: NewHeaderAnalyzer(), } diff --git a/pkg/analyzer/report_test.go b/pkg/analyzer/report_test.go index bf413ce..5a325b1 100644 --- a/pkg/analyzer/report_test.go +++ b/pkg/analyzer/report_test.go @@ -32,7 +32,7 @@ import ( ) func TestNewReportGenerator(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) if gen == nil { t.Fatal("Expected report generator, got nil") } @@ -55,7 +55,7 @@ func TestNewReportGenerator(t *testing.T) { } func TestAnalyzeEmail(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) email := createTestEmail() @@ -75,7 +75,7 @@ func TestAnalyzeEmail(t *testing.T) { } func TestGenerateReport(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) testID := uuid.New() email := createTestEmail() @@ -130,7 +130,7 @@ func TestGenerateReport(t *testing.T) { } func TestGenerateReportWithSpamAssassin(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) testID := uuid.New() email := createTestEmailWithSpamAssassin() @@ -150,7 +150,7 @@ func TestGenerateReportWithSpamAssassin(t *testing.T) { } func TestGenerateRawEmail(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) tests := []struct { name string