From ab992abc268252720acfaae518bf66dcad8c57e4 Mon Sep 17 00:00:00 2001 From: thuanle Date: Tue, 28 Apr 2026 06:26:56 +0700 Subject: [PATCH] feat(rules): add BlocklistRule Co-Authored-By: Claude Opus 4.7 --- internal/rules/blocklist.go | 26 ++++++++++++++++++++++++ internal/rules/blocklist_test.go | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 internal/rules/blocklist.go create mode 100644 internal/rules/blocklist_test.go diff --git a/internal/rules/blocklist.go b/internal/rules/blocklist.go new file mode 100644 index 0000000..643b6aa --- /dev/null +++ b/internal/rules/blocklist.go @@ -0,0 +1,26 @@ +package rules + +import "strings" + +// BlocklistRule rejects emails from configured senders. +type BlocklistRule struct { + emails map[string]bool +} + +// NewBlocklistRule creates a blocklist rule from a list of email addresses. +func NewBlocklistRule(emails []string) *BlocklistRule { + m := make(map[string]bool, len(emails)) + for _, e := range emails { + m[strings.ToLower(e)] = true + } + return &BlocklistRule{emails: m} +} + +func (r *BlocklistRule) Name() string { return "blocklist" } + +func (r *BlocklistRule) Evaluate(ctx EmailContext) RuleResult { + if r.emails[strings.ToLower(ctx.Sender)] { + return Reject("blocked sender") + } + return Accept() +} diff --git a/internal/rules/blocklist_test.go b/internal/rules/blocklist_test.go new file mode 100644 index 0000000..3081885 --- /dev/null +++ b/internal/rules/blocklist_test.go @@ -0,0 +1,35 @@ +package rules + +import "testing" + +func TestBlocklistRule_SenderBlocked_Rejects(t *testing.T) { + r := NewBlocklistRule([]string{"spam@test.com"}) + result := r.Evaluate(EmailContext{Sender: "spam@test.com"}) + if result.Accepted { + t.Fatal("expected reject for blocked sender") + } +} + +func TestBlocklistRule_SenderNotBlocked_Accepts(t *testing.T) { + r := NewBlocklistRule([]string{"spam@test.com"}) + result := r.Evaluate(EmailContext{Sender: "ok@test.com"}) + if !result.Accepted { + t.Fatal("expected accept for non-blocked sender") + } +} + +func TestBlocklistRule_EmptyList_Accepts(t *testing.T) { + r := NewBlocklistRule(nil) + result := r.Evaluate(EmailContext{Sender: "anyone@test.com"}) + if !result.Accepted { + t.Fatal("empty blocklist should accept all") + } +} + +func TestBlocklistRule_CaseInsensitive(t *testing.T) { + r := NewBlocklistRule([]string{"Spam@Test.com"}) + result := r.Evaluate(EmailContext{Sender: "spam@test.com"}) + if result.Accepted { + t.Fatal("blocklist should be case-insensitive") + } +}