Files
mailgoserver/internal/smtpserver/mailbox_autoreply_test.go
T

103 lines
4.1 KiB
Go
Raw Normal View History

2026-08-20 15:55:40 +01:00
package smtpserver
import (
"net/smtp"
"testing"
"mailgoserver/internal/db"
)
func sendFrom(t *testing.T, addr, from, to, 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(from); err != nil {
t.Fatalf("MAIL FROM: %v", err)
}
if err := c.Rcpt(to); 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()
}
// TestAutoReplyRuleFiresOnceThenSuppressedFor24h confirms an 'auto_reply' filter rule
// sends a reply on the first matching message from a sender, still stores the
// original message normally in INBOX (auto-reply is additive, not a replacement
// action), and suppresses a second reply to the same sender within the loop-prevention
// window — without needing to wait a real 24h, by directly manipulating the DB layer
// HasRecentAutoReply/RecordAutoReply already tested at unit-test speed.
func TestAutoReplyRuleFiresOnceThenSuppressedFor24h(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
conditions := []db.RuleCondition{{Field: "subject", Op: "contains", Value: "vacation"}}
if _, err := backend.DB.CreateRuleMulti(mailboxID, 0, conditions, "all", "", "auto_reply", "Out of office", `{"body":"I am away."}`); err != nil {
t.Fatal(err)
}
addr := startTestServer(t, backend)
if err := sendFrom(t, addr, "sender@example.org", "inbox@example.com", "vacation question"); err != nil {
t.Fatalf("first send: %v", err)
}
// The message itself must still be stored normally — auto_reply doesn't divert it.
msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
if err != nil || len(msgs) != 1 {
t.Fatalf("expected the original message still stored in INBOX, got %d (err=%v)", len(msgs), err)
}
// Loop-prevention must now be recorded for this sender.
recent, err := backend.DB.HasRecentAutoReply(mailboxID, "sender@example.org")
if err != nil || !recent {
t.Fatalf("expected an auto-reply to be recorded, HasRecentAutoReply = %v, err=%v", recent, err)
}
// A second matching message from the same sender within the window must not
// record a second auto-reply (RecordAutoReply only called once).
if err := sendFrom(t, addr, "sender@example.org", "inbox@example.com", "vacation again"); err != nil {
t.Fatalf("second send: %v", err)
}
var count int
if err := backend.DB.QueryRow(`SELECT COUNT(*) FROM esrv_mailbox_autoreply_log WHERE mailbox_id = ? AND sender_addr = ?`, mailboxID, "sender@example.org").Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatalf("expected exactly 1 auto-reply log row after two messages within the window, got %d", count)
}
}
// TestAutoReplyNeverFiresForNullSender confirms sendAutoReply itself refuses a
// null-sender message — the classic bounce-loop bug this codebase already avoids for
// SendBounce. A white-box unit test, not a live SMTP round trip: Session.Mail()
// already hard-rejects MAIL FROM:<> for every sender category before DATA is ever
// reached (a separate, pre-existing gap — this server currently can't accept a
// genuine bounce/DSN from another real MTA at all — out of scope for this feature),
// so sendAutoReply's own guard is a defense-in-depth check that isn't reachable
// through the live protocol today; this test exercises it directly instead.
func TestAutoReplyNeverFiresForNullSender(t *testing.T) {
backend, mailboxID := newTestBackendWithMailbox(t)
mbox, err := backend.DB.GetMailboxByID(mailboxID)
if err != nil || mbox == nil {
t.Fatal(err)
}
s := &Session{backend: backend, mailFrom: ""}
s.sendAutoReply(mbox, "Out of office", "away", "")
var count int
if err := backend.DB.QueryRow(`SELECT COUNT(*) FROM esrv_mailbox_autoreply_log WHERE mailbox_id = ?`, mailboxID).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("expected no auto-reply recorded for a null-sender session, got %d log row(s)", count)
}
}