Compare commits

...

2 commits

Author SHA1 Message Date
871f4e62f6 Fix content scoring error
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-27 17:56:06 +07:00
86ec7a6100 By default, only check the first IP against RBL, not all chain 2025-10-27 17:56:06 +07:00
8 changed files with 36 additions and 24 deletions

View file

@ -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.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.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.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.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.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") flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey")

View file

@ -61,9 +61,10 @@ type EmailConfig struct {
// AnalysisConfig contains timeout and behavior settings for email analysis // AnalysisConfig contains timeout and behavior settings for email analysis
type AnalysisConfig struct { type AnalysisConfig struct {
DNSTimeout time.Duration DNSTimeout time.Duration
HTTPTimeout time.Duration HTTPTimeout time.Duration
RBLs []string RBLs []string
CheckAllIPs bool // Check all IPs found in headers, not just the first one
} }
// DefaultConfig returns a configuration with sensible defaults // DefaultConfig returns a configuration with sensible defaults
@ -86,6 +87,7 @@ func DefaultConfig() *Config {
DNSTimeout: 5 * time.Second, DNSTimeout: 5 * time.Second,
HTTPTimeout: 10 * time.Second, HTTPTimeout: 10 * time.Second,
RBLs: []string{}, RBLs: []string{},
CheckAllIPs: false, // By default, only check the first IP
}, },
} }
} }

View file

@ -44,6 +44,7 @@ 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.CheckAllIPs,
) )
return &EmailAnalyzer{ return &EmailAnalyzer{

View file

@ -751,7 +751,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s
brokenLinks++ brokenLinks++
} }
} }
score += 20 * brokenLinks / len(results.Links) score += 20 * (len(results.Links) - brokenLinks) / len(results.Links)
// Too much links, 10 points penalty // Too much links, 10 points penalty
if len(results.Links) > 30 { if len(results.Links) > 30 {
score -= 10 score -= 10
@ -769,7 +769,7 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, s
noAltCount++ noAltCount++
} }
} }
score += 15 * noAltCount / len(results.Images) score += 15 * (len(results.Images) - noAltCount) / len(results.Images)
} else { } else {
// No images is Ok // No images is Ok
score += 15 score += 15

View file

@ -34,9 +34,10 @@ import (
// RBLChecker checks IP addresses against DNS-based blacklists // RBLChecker checks IP addresses against DNS-based blacklists
type RBLChecker struct { type RBLChecker struct {
Timeout time.Duration Timeout time.Duration
RBLs []string RBLs []string
resolver *net.Resolver 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 // 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 // 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 { if timeout == 0 {
timeout = 5 * time.Second // Default timeout timeout = 5 * time.Second // Default timeout
} }
@ -58,8 +59,9 @@ func NewRBLChecker(timeout time.Duration, rbls []string) *RBLChecker {
rbls = DefaultRBLs rbls = DefaultRBLs
} }
return &RBLChecker{ return &RBLChecker{
Timeout: timeout, Timeout: timeout,
RBLs: rbls, RBLs: rbls,
CheckAllIPs: checkAllIPs,
resolver: &net.Resolver{ resolver: &net.Resolver{
PreferGo: true, PreferGo: true,
}, },
@ -96,6 +98,11 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults {
results.ListedCount++ results.ListedCount++
} }
} }
// Only check the first IP unless CheckAllIPs is enabled
if !r.CheckAllIPs {
break
}
} }
return results return results

View file

@ -55,7 +55,7 @@ func TestNewRBLChecker(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) {
checker := NewRBLChecker(tt.timeout, tt.rbls) checker := NewRBLChecker(tt.timeout, tt.rbls, false)
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)
} }
@ -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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) listedIPs := checker.GetUniqueListedIPs(results)
expectedIPs := []string{"198.51.100.1", "198.51.100.2"} 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 { tests := []struct {
name string name string

View file

@ -44,12 +44,13 @@ func NewReportGenerator(
dnsTimeout time.Duration, dnsTimeout time.Duration,
httpTimeout time.Duration, httpTimeout time.Duration,
rbls []string, rbls []string,
checkAllIPs bool,
) *ReportGenerator { ) *ReportGenerator {
return &ReportGenerator{ return &ReportGenerator{
authAnalyzer: NewAuthenticationAnalyzer(), authAnalyzer: NewAuthenticationAnalyzer(),
spamAnalyzer: NewSpamAssassinAnalyzer(), spamAnalyzer: NewSpamAssassinAnalyzer(),
dnsAnalyzer: NewDNSAnalyzer(dnsTimeout), dnsAnalyzer: NewDNSAnalyzer(dnsTimeout),
rblChecker: NewRBLChecker(dnsTimeout, rbls), rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs),
contentAnalyzer: NewContentAnalyzer(httpTimeout), contentAnalyzer: NewContentAnalyzer(httpTimeout),
headerAnalyzer: NewHeaderAnalyzer(), headerAnalyzer: NewHeaderAnalyzer(),
} }

View file

@ -32,7 +32,7 @@ import (
) )
func TestNewReportGenerator(t *testing.T) { 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 { 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) 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) 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) 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) gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false)
tests := []struct { tests := []struct {
name string name string