Files
mailgoserver/internal/smtpserver/mailbox_rules_test.go
T

357 lines
11 KiB
Go

package smtpserver
import (
"net/smtp"
"strings"
"testing"
"mailgoserver/internal/db"
)
func TestBlockedSenderRejectedAtRcpt(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "block", "test@example.com"); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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)
}
err = c.Rcpt("inbox@example.com")
if err == nil {
t.Fatal("expected RCPT to be rejected for a blocked sender")
}
if !strings.Contains(err.Error(), "550") {
t.Fatalf("expected 550, got: %v", err)
}
}
// TestAllowListBypassesSpamQuarantine confirms a zero reject threshold quarantines a
// non-allow-listed sender's mail into Spam (still accepted at SMTP level — spam is
// stored for review, not silently bounced), while an allow-listed sender's mail
// skips scoring entirely and lands in INBOX as normal.
func TestAllowListBypassesSpamQuarantine(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
// Force every non-allow-listed message to be quarantined as spam.
backend.Cfg.Section("Mailstore").Key("spam_reject_score").SetValue("0")
send := func(t *testing.T) error {
t.Helper()
c, err := smtp.Dial(startTestServer(t, backend))
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: hi\r\n\r\nhi"))
return w.Close()
}
if err := send(t); err != nil {
t.Fatalf("expected delivery accepted (quarantined) with a zero reject threshold and no allow-list entry, got: %v", err)
}
spamMsgs, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
if err != nil || len(spamMsgs) != 1 {
t.Fatalf("expected 1 quarantined message in Spam, got %d (err=%v)", len(spamMsgs), err)
}
if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "allow", "test@example.com"); err != nil {
t.Fatal(err)
}
if err := send(t); err != nil {
t.Fatalf("expected delivery to succeed once the sender is allow-listed, got: %v", err)
}
inboxMsgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil || len(inboxMsgs) != 1 {
t.Fatalf("expected 1 message in INBOX once allow-listed (spam scoring skipped), got %d (err=%v)", len(inboxMsgs), err)
}
}
// TestJunkListQuarantinesWithoutRejecting confirms a "junk" allowblock entry (the
// self-service webmail Blocklist — distinct from "block", which hard-rejects at RCPT
// instead) is still accepted at SMTP level but always lands in Junk, bypassing spam
// scoring entirely — the same soft-quarantine outcome as a heuristic/rspamd score hit,
// but decided unconditionally rather than computed.
func TestJunkListQuarantinesWithoutRejecting(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "junk", "test@example.com"); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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("expected RCPT accepted (junk quarantines, doesn't reject), got: %v", err)
}
w, err := c.Data()
if err != nil {
t.Fatal(err)
}
w.Write([]byte("Subject: hi\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("DATA: %v", err)
}
inbox, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(inbox) != 0 {
t.Fatalf("expected nothing in INBOX, found %d", len(inbox))
}
junk, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
if err != nil {
t.Fatal(err)
}
if len(junk) != 1 {
t.Fatalf("expected 1 message quarantined in Junk, got %d", len(junk))
}
}
func TestFilterRuleDeleteDropsMessage(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "drop-me", "delete", ""); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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: drop-me please\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("expected DATA to still report success even though the rule drops the message, got: %v", err)
}
msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(msgs) != 0 {
t.Fatalf("expected the delete rule to prevent storage, found %d messages", len(msgs))
}
}
func TestFilterRuleMarkReadSetsSeenFlag(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "newsletter", "mark_read", ""); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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: weekly newsletter\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("DATA: %v", err)
}
msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(msgs) != 1 {
t.Fatalf("expected 1 message, got %d", len(msgs))
}
if !strings.Contains(msgs[0].Flags, `\Seen`) {
t.Fatalf("expected the mark_read rule to set \\Seen, got flags %q", msgs[0].Flags)
}
}
// TestFilterRuleForwardKeepCopyFalseDropsLocalCopy confirms a forward rule with
// keep_copy=false doesn't also store a local copy — forwarding itself happens
// fire-and-forget in a goroutine (see deliverLocally), so this only asserts on the
// local-storage decision, not on the (real, DNS-dependent) outbound delivery
// succeeding. ".invalid" is an RFC 2606 TLD guaranteed to never resolve, so the
// background forward attempt fails fast instead of hanging the test on a timeout.
func TestFilterRuleForwardKeepCopyFalseDropsLocalCopy(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
conditions := []db.RuleCondition{{Field: "subject", Op: "contains", Value: "fwd-drop"}}
if _, err := backend.DB.CreateRuleMulti(mailboxID, 0, conditions, "all", "", "forward", "elsewhere@forward-test.invalid", `{"keep_copy":false}`); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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: fwd-drop this\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("expected DATA to still report success even though the rule forwards-and-drops, got: %v", err)
}
msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(msgs) != 0 {
t.Fatalf("expected forward with keep_copy=false to skip local storage, found %d messages", len(msgs))
}
}
// TestFilterRuleForwardKeepCopyTrueStillStoresLocally confirms the default
// (keep_copy=true) still stores the mailbox owner's own copy alongside forwarding.
func TestFilterRuleForwardKeepCopyTrueStillStoresLocally(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
conditions := []db.RuleCondition{{Field: "subject", Op: "contains", Value: "fwd-keep"}}
if _, err := backend.DB.CreateRuleMulti(mailboxID, 0, conditions, "all", "", "forward", "elsewhere@forward-test.invalid", `{"keep_copy":true}`); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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: fwd-keep this\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("DATA: %v", err)
}
msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(msgs) != 1 {
t.Fatalf("expected forward with keep_copy=true to still store locally, found %d messages", len(msgs))
}
}
func TestFilterRuleMoveToFolderStoresInNamedFolder(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "spam", "move_to_folder", "Junk"); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
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: this looks like spam\r\n\r\nhi"))
if err := w.Close(); err != nil {
t.Fatalf("DATA: %v", err)
}
inbox, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(inbox) != 0 {
t.Fatalf("expected nothing in INBOX, found %d", len(inbox))
}
spam, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
if err != nil {
t.Fatal(err)
}
if len(spam) != 1 {
t.Fatalf("expected 1 message in Spam, got %d", len(spam))
}
}