package smtpserver import ( "encoding/json" "net/http" "net/http/httptest" "net/smtp" "testing" ) // fakeRspamd stands in for a real rspamd instance, always returning the fixed // score/action given — enough to exercise deliverLocally's rspamd branch without a // live rspamd deployment. func fakeRspamd(t *testing.T, score float64, action string) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{"score": score, "action": action}) })) t.Cleanup(srv.Close) return srv } func sendTestMessage(t *testing.T, addr, subject string) error { t.Helper() c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil { t.Fatalf("auth: %v", err) } if err := c.Mail("test@example.com"); err != nil { t.Fatalf("MAIL FROM: %v", err) } if err := c.Rcpt("inbox@example.com"); err != nil { t.Fatalf("RCPT: %v", err) } w, err := c.Data() if err != nil { t.Fatal(err) } w.Write([]byte("Subject: " + subject + "\r\n\r\nhi")) return w.Close() } // TestRspamdExplicitRejectActionStillHardRejects confirms rspamd's own "reject" // action still hard-rejects at SMTP time (unlike a bare score-threshold hit, which is // quarantined to Spam instead — see TestRspamdScoreThresholdQuarantinesInsteadOfRejecting). func TestRspamdExplicitRejectActionStillHardRejects(t *testing.T) { backend, mailboxID := newTestBackendWithMailbox(t) rspamd := fakeRspamd(t, 20, "reject") backend.Cfg.Section("Rspamd").Key("enabled").SetValue("true") backend.Cfg.Section("Rspamd").Key("url").SetValue(rspamd.URL) addr := startTestServer(t, backend) if err := sendTestMessage(t, addr, "hi"); err == nil { t.Fatal("expected delivery to be hard-rejected when rspamd's action is \"reject\"") } inboxMsgs, _ := backend.DB.ListMessagesInFolder(mailboxID, "INBOX") spamMsgs, _ := backend.DB.ListMessagesInFolder(mailboxID, "Spam") if len(inboxMsgs) != 0 || len(spamMsgs) != 0 { t.Fatalf("expected nothing stored anywhere for a hard reject, got INBOX=%d Spam=%d", len(inboxMsgs), len(spamMsgs)) } } // TestRspamdScoreThresholdQuarantinesInsteadOfRejecting confirms a bare rspamd score // over the configured threshold (action something other than "reject") is accepted // and quarantined into Spam, not bounced. func TestRspamdScoreThresholdQuarantinesInsteadOfRejecting(t *testing.T) { backend, mailboxID := newTestBackendWithMailbox(t) rspamd := fakeRspamd(t, 20, "add header") backend.Cfg.Section("Rspamd").Key("enabled").SetValue("true") backend.Cfg.Section("Rspamd").Key("url").SetValue(rspamd.URL) backend.Cfg.Section("Rspamd").Key("reject_score").SetValue("15") addr := startTestServer(t, backend) if err := sendTestMessage(t, addr, "hi"); err != nil { t.Fatalf("expected delivery accepted (quarantined), got: %v", err) } spamMsgs, err := backend.DB.ListMessagesInFolder(mailboxID, "Spam") if err != nil || len(spamMsgs) != 1 { t.Fatalf("expected 1 quarantined message in Spam, got %d (err=%v)", len(spamMsgs), err) } }