2026-08-20 15:55:40 +01:00
|
|
|
package smtpserver
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/smtp"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"mailgoserver/internal/db"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestDomainSendRateLimited is a white-box unit test of the core rate-limit decision
|
|
|
|
|
// (no real SMTP transaction, no network) — see TestSendRateLimitRejectsOverLimitMail
|
|
|
|
|
// below for the live end-to-end wiring check.
|
|
|
|
|
func TestDomainSendRateLimited(t *testing.T) {
|
|
|
|
|
backend := newTestBackend(t)
|
|
|
|
|
domainID, err := backend.DB.CreateDomain("ratelimited.example")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
limit := 2
|
|
|
|
|
if err := backend.DB.SetDomainSendRateLimit(domainID, &limit); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 06:45:05 +01:00
|
|
|
if limited, err := backend.domainSendRateLimited("ratelimited.example"); err != nil || limited {
|
2026-08-20 15:55:40 +01:00
|
|
|
t.Fatalf("expected not limited with zero sends so far, limited=%v err=%v", limited, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
if _, err := backend.DB.InsertEmailLog(db.EmailLog{
|
|
|
|
|
MessageID: "m" + string(rune('a'+i)) + "@ratelimited.example", Timestamp: time.Now(),
|
|
|
|
|
MailFrom: "sender@ratelimited.example", EmailHeaders: "h", Status: "relayed",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-22 06:45:05 +01:00
|
|
|
if limited, err := backend.domainSendRateLimited("ratelimited.example"); err != nil || !limited {
|
2026-08-20 15:55:40 +01:00
|
|
|
t.Fatalf("expected limited after hitting the cap of 2, limited=%v err=%v", limited, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An unconfigured domain (no limit set) must never limit, regardless of volume.
|
2026-08-22 06:45:05 +01:00
|
|
|
if limited, err := backend.domainSendRateLimited("example.com"); err != nil || limited {
|
2026-08-20 15:55:40 +01:00
|
|
|
t.Fatalf("expected no limit for a domain with send_rate_limit_per_hour unset, limited=%v err=%v", limited, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An unrecognized domain must never limit either (not this server's problem to
|
|
|
|
|
// cap, and GetDomainByName returning nil must fail open, not error).
|
2026-08-22 06:45:05 +01:00
|
|
|
if limited, err := backend.domainSendRateLimited("nowhere.invalid"); err != nil || limited {
|
2026-08-20 15:55:40 +01:00
|
|
|
t.Fatalf("expected no limit for an unrecognized domain, limited=%v err=%v", limited, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestSendRateLimitRejectsOverLimitMail confirms the real wiring: an authorized
|
|
|
|
|
// sender's relay recipient gets a 450 with a rate-limit reason recorded once the
|
|
|
|
|
// domain's cap is hit, via a real SMTP transaction end to end.
|
|
|
|
|
func TestSendRateLimitRejectsOverLimitMail(t *testing.T) {
|
|
|
|
|
backend, _ := newTestBackendWithMailbox(t)
|
|
|
|
|
domains, err := backend.DB.ListDomains()
|
|
|
|
|
if err != nil || len(domains) == 0 {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
limit := 0
|
|
|
|
|
if err := backend.DB.SetDomainSendRateLimit(domains[0].ID, &limit); 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("someone@elsewhere.example"); 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"))
|
|
|
|
|
// The final response is the same generic 550 either way (see Data()'s fixed
|
|
|
|
|
// message for a total failure) — what actually distinguishes "rate limited" from
|
|
|
|
|
// "a real relay failure" is the recorded per-recipient reason, checked below.
|
|
|
|
|
w.Close()
|
|
|
|
|
|
|
|
|
|
var errMsg string
|
|
|
|
|
if err := backend.DB.QueryRow(`SELECT error_message FROM esrv_email_recipient_logs ORDER BY id DESC LIMIT 1`).Scan(&errMsg); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if errMsg == "" || !strings.Contains(errMsg, "rate limit") {
|
|
|
|
|
t.Fatalf("expected the recipient log's error to mention the rate limit, got %q", errMsg)
|
|
|
|
|
}
|
|
|
|
|
}
|