86 lines
3.1 KiB
Go
86 lines
3.1 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|