Files
mailgoserver/internal/smtpserver/bounce_test.go
T

102 lines
3.2 KiB
Go

package smtpserver
import (
"net/smtp"
"testing"
"mailgoserver/internal/db"
"mailgoserver/internal/mailstore"
)
// createTestMailboxWithQuota mirrors newTestBackendWithMailbox's inline mailbox setup,
// parameterized by quota so this file can create both a normal and an
// effectively-always-full mailbox.
func createTestMailboxWithQuota(t *testing.T, backend *Backend, store *mailstore.Store, email string, quotaBytes int64) int64 {
t.Helper()
dek := mailstore.GenerateDEK()
wrapped, nonce, err := store.WrapDEK(dek)
if err != nil {
t.Fatal(err)
}
hash, err := db.HashPassword("portal-password-unused")
if err != nil {
t.Fatal(err)
}
mailboxID, err := backend.DB.CreateMailbox(email, hash, 1, quotaBytes, wrapped, nonce)
if err != nil {
t.Fatal(err)
}
return mailboxID
}
// TestPartialLocalDeliveryFailureBouncesAndAccepts confirms a multi-recipient
// transaction where one local mailbox accepts the message and another can't (quota
// exceeded, discovered only during DATA — RCPT can't catch it) is accepted (250, not
// 550 — the successful recipient already has it, so the client must not retry the
// whole transaction) and that the sender gets a bounce in their own mailbox describing
// the recipient that failed.
func TestPartialLocalDeliveryFailureBouncesAndAccepts(t *testing.T) {
backend, okMailboxID := newTestBackendWithMailbox(t)
store := backend.Mailstore
fullMailboxID := createTestMailboxWithQuota(t, backend, store, "full@example.com", 1)
senderMailboxID := createTestMailboxWithQuota(t, backend, store, "test@example.com", 5*1024*1024*1024)
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 (ok mailbox): %v", err)
}
if err := c.Rcpt("full@example.com"); err != nil {
t.Fatalf("RCPT (over-quota mailbox, still accepted at RCPT time): %v", err)
}
w, err := c.Data()
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte("Subject: hello\r\n\r\nhi there")); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatalf("expected DATA to succeed (250, partial success) despite one recipient failing, got: %v", err)
}
okMsgs, err := backend.DB.ListMessagesInFolder(okMailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(okMsgs) != 1 {
t.Fatalf("expected the message delivered to inbox@example.com, got %d messages", len(okMsgs))
}
fullMsgs, err := backend.DB.ListMessagesInFolder(fullMailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(fullMsgs) != 0 {
t.Fatalf("expected no message delivered to the over-quota mailbox, got %d", len(fullMsgs))
}
bounces, err := backend.DB.ListMessagesInFolder(senderMailboxID, "INBOX")
if err != nil {
t.Fatal(err)
}
if len(bounces) != 1 {
t.Fatalf("expected 1 bounce message in the sender's own mailbox, got %d", len(bounces))
}
if bounces[0].CachedSubject != "Undelivered Mail Returned to Sender" {
t.Errorf("bounce subject = %q", bounces[0].CachedSubject)
}
}