103 lines
4.1 KiB
Go
103 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|